commit b88bf3e58fb7ec83b9219fb975b95a8cae82fb60 Author: Vivek Teega Date: Sun May 1 16:47:07 2022 +0530 First/Base commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b8eeeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..67e6611 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Economic System for FLOBNB + +Homepage to find all the details related to FLOBnb project +# economicsystem-mining diff --git a/css/main.css b/css/main.css new file mode 100644 index 0000000..99c9ba1 --- /dev/null +++ b/css/main.css @@ -0,0 +1,536 @@ +/* +Add CSS variables here to change CSS of components +Suppose you want to customize button from component, just add + +sm-button { +--background : #CCC; +} + +*/ + +* { + padding: 0; + margin: 0; + box-sizing: border-box; + font-family: "Roboto", sans-serif; +} + +:root { + font-size: clamp(1rem, 1.2vmax, 3rem); +} + +html, +body { + height: 100%; + scroll-behavior: smooth; +} + +body { + color: rgba(var(--text-color), 1); + background: rgba(var(--background-color), 1); +} +body, +body * { + --accent-color: #0d7377; + --text-color: 17, 17, 17; + --background-color: 255, 255, 255; + --danger-color: red; +} + +body[data-theme="dark"], +body[data-theme="dark"] * { + --accent-color: #32e0c4; + --text-color: 240, 240, 240; + --text-color-light: 170, 170, 170; + --background-color: 10, 10, 10; + --danger-color: rgb(255, 106, 106); +} + +p { + font-size: 0.8; + max-width: 65ch; + line-height: 1.7; + margin-bottom: 1.5rem; + color: rgba(var(--text-color), 0.8); +} +p:not(:last-of-type) { + margin-bottom: 1rem; +} + +img { + object-fit: cover; +} + +a { + color: inherit; + text-decoration: none; +} +a:focus-visible { + box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset; +} + +button { + display: inline-flex; + border: none; + background-color: inherit; +} + +a:any-link:focus-visible { + outline: rgba(var(--text-color), 1) 0.1rem solid; +} + +sm-button { + --border-radius: 0.3rem; +} + +ul { + list-style: none; +} + +.flex { + display: flex; +} + +.grid { + display: grid; +} + +.hide { + opacity: 0; + pointer-events: none; +} + +.hide { + display: none !important; +} + +.no-transformations { + transform: none !important; +} + +.overflow-ellipsis { + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.breakable { + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +.full-bleed { + grid-column: 1/4; +} + +.h1 { + font-size: 2.5rem; +} + +.h2 { + font-size: 2rem; +} + +.h3 { + font-size: 1.4rem; +} + +.h4 { + font-size: 1rem; +} + +.h5 { + font-size: 0.8rem; +} + +.uppercase { + text-transform: uppercase; +} + +.capitalize { + text-transform: capitalize; +} + +.flex { + display: flex; +} + +.grid { + display: grid; +} + +.grid-3 { + grid-template-columns: 1fr auto auto; +} + +.flow-column { + grid-auto-flow: column; +} + +.gap-0-5 { + gap: 0.5rem; +} + +.gap-1 { + gap: 1rem; +} + +.gap-1-5 { + gap: 1.5rem; +} + +.gap-2 { + gap: 2rem; +} + +.gap-3 { + gap: 3rem; +} + +.text-align-right { + text-align: right; +} + +.align-start { + align-items: flex-start; +} + +.align-center { + align-items: center; +} + +.text-center { + text-align: center; +} + +.justify-start { + justify-content: start; +} + +.justify-center { + justify-content: center; +} + +.justify-right { + margin-left: auto; +} + +.align-self-center { + align-self: center; +} + +.justify-self-center { + justify-self: center; +} + +.justify-self-start { + justify-self: start; +} + +.justify-self-end { + justify-self: end; +} + +.direction-column { + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.w-100 { + width: 100%; +} + +.color-0-8 { + color: rgba(var(--text-color), 0.8); +} + +.weight-400 { + font-weight: 400; +} + +.weight-500 { + font-weight: 500; +} + +.ripple { + position: absolute; + border-radius: 50%; + transform: scale(0); + background: rgba(var(--text-color), 0.16); + pointer-events: none; +} + +.interact { + position: relative; + overflow: hidden; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.observe-empty-state:empty { + display: none; +} + +.observe-empty-state:not(:empty) ~ .empty-state { + display: none; +} + +.icon { + width: 1.5rem; + height: 1.5rem; + fill: rgba(var(--text-color), 0.9); +} + +.button__icon { + height: 1.2rem; + width: 1.2rem; +} +.button__icon--left { + margin-right: 0.5rem; +} +.button__icon--right { + margin-left: 0.5rem; +} + +#confirmation_popup, +#prompt_popup { + flex-direction: column; +} +#confirmation_popup h4, +#prompt_popup h4 { + font-weight: 500; + margin-bottom: 0.5rem; +} +#confirmation_popup sm-button, +#prompt_popup sm-button { + margin: 0; +} +#confirmation_popup .flex, +#prompt_popup .flex { + padding: 0; + margin-top: 1rem; +} +#confirmation_popup .flex sm-button:first-of-type, +#prompt_popup .flex sm-button:first-of-type { + margin-right: 0.6rem; + margin-left: auto; +} + +h1, +h2, +h3, +h4.h5 { + font-family: "Poppins", sans-serif; +} + +h2 { + margin: 3rem 0 1rem 0; + text-transform: capitalize; +} + +main { + display: grid; + height: 100%; + grid-template-rows: auto 1fr auto; + grid-template-areas: "main-header" "." "side-nav"; +} + +#main_header { + grid-area: main-header; + padding: 1rem 1.5rem; + border-bottom: 1px solid rgba(var(--text-color), 0.1); + gap:0.5rem; +} + +#logo { + display: grid; + align-items: center; + width: 100%; + grid-template-columns: auto 1fr; + gap: 0 0.5rem; + margin-right: 1rem; +} +#logo h4 { + text-transform: capitalize; + font-size: 1rem; + font-weight: 600; +} +#logo #main_logo { + height: 1.4rem; + width: 1.4rem; + fill: rgba(var(--text-color), 1); + stroke: none; +} + +.right { + max-height: 100%; + overflow-y: auto; +} + +#side_nav { + grid-area: side-nav; +} +#side_nav h4 { + font-size: 0.9rem; + letter-spacing: 0.08em; + text-transform: uppercase; + padding: 1.5rem; +} + +.nav-list { + list-style: none; + display: flex; + justify-content: space-evenly; +} +.nav-list li { + width: 100%; +} + +.nav-list__item { + display: flex; + padding: 0.8rem 1.5rem; + text-transform: capitalize; +} +.nav-list__item--active { + color: var(--accent-color); +} +.nav-list__item--active .icon { + fill: var(--accent-color); +} + +.right { + padding: 1.5rem; +} +.right h1 { + margin-bottom: 1.5rem; +} + +.page { + display: flex; + flex-direction: column; + padding-bottom: 3rem; +} + +.card-wrapper { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); + gap: 1.5rem; +} + +.card { + padding: 1.5rem; + display: flex; + flex-direction: column; + border-radius: 0.5rem; + background-color: rgba(var(--text-color), 0.06); +} + +.card h3 { + font-weight: 500; +} + +.popup__header { + display: grid; + gap: 0.5rem; + width: 100%; + padding: 0 1.5rem 0 0.8rem; + align-items: center; + grid-template-columns: auto 1fr auto; +} + +.popup__header__close { + padding: 0.5rem; + cursor: pointer; +} + + +@media screen and (max-width: 640px) { + main { + grid-template-rows: auto 1fr; + grid-template-columns: 1fr; + } + + .nav-list__item { + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0.4rem; + } + .nav-list__item span { + font-size: 0.8rem; + margin-top: 0.3rem; + } +} +@media screen and (min-width: 640px) { + sm-popup { + --width: 32rem; + } + + main { + grid-template-columns: 14rem minmax(0, 1fr); + grid-template-rows: auto 1fr; + grid-template-areas: "main-header main-header" "side-nav ."; + } + + .nav-list { + flex-direction: column; + } + + .nav-list__item { + align-items: center; + justify-content: start; + } + .nav-list__item--active { + background: rgba(var(--text-color), 0.06); + } + .nav-list__item .icon { + margin-right: 0.5rem; + } + + .right { + display: grid; + grid-template-columns: 1fr 90% 1fr; + } + .right > * { + grid-column: 2/3; + } + + .page__title { + font-size: 2.5rem; + } + + .popup__header { + grid-column: 1/-1; + padding: 1rem 1.5rem 0 0.8rem; + } +} +@media (any-hover: hover) { + ::-webkit-scrollbar { + width: 0.5rem; + height: 0.5rem; + } + + ::-webkit-scrollbar-thumb { + background: rgba(var(--text-color), 0.3); + border-radius: 1rem; + } + ::-webkit-scrollbar-thumb:hover { + background: rgba(var(--text-color), 0.5); + } + + .nav-list__item:hover { + background: rgba(var(--text-color), 0.1); + cursor: pointer; + } + +} diff --git a/css/main.css.map b/css/main.css.map new file mode 100644 index 0000000..d5bf903 --- /dev/null +++ b/css/main.css.map @@ -0,0 +1,9 @@ +{ + "version": 3, + "mappings": "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", + "sources": [ + "main.scss" + ], + "names": [], + "file": "main.css" +} \ No newline at end of file diff --git a/css/main.min.css b/css/main.min.css new file mode 100644 index 0000000..5982293 --- /dev/null +++ b/css/main.min.css @@ -0,0 +1 @@ +.hide,.ripple{pointer-events:none}.nav-list,ul{list-style:none}*{padding:0;margin:0;box-sizing:border-box;font-family:Roboto,sans-serif}:root{font-size:clamp(1rem,1.2vmax,3rem)}body,html{height:100%;scroll-behavior:smooth}body{color:rgba(var(--text-color),1);background:rgba(var(--background-color),1)}body,body *{--accent-color:#0D7377;--text-color:17,17,17;--background-color:255,255,255;--danger-color:red}body[data-theme=dark],body[data-theme=dark] *{--accent-color:#32E0C4;--text-color:240,240,240;--text-color-light:170,170,170;--background-color:10,10,10;--danger-color:rgb(255, 106, 106)}p{font-size:.8;max-width:65ch;line-height:1.7;margin-bottom:1.5rem;color:rgba(var(--text-color),.8)}p:not(:last-of-type){margin-bottom:1rem}img{object-fit:cover}a{color:inherit;text-decoration:none}a:focus-visible{box-shadow:0 0 0 .1rem rgba(var(--text-color),1) inset}button{display:inline-flex;border:none;background-color:inherit}a:any-link:focus-visible{outline:solid rgba(var(--text-color),1)}sm-button{--border-radius:0.3rem}.hide{opacity:0}.hide-completely{display:none!important}.no-transformations{transform:none!important}.overflow-ellipsis{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.breakable{overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.full-bleed{grid-column:1/4}.h1{font-size:2.5rem}.h2{font-size:2rem}.h3{font-size:1.4rem}#logo h4,.h4{font-size:1rem}.h5{font-size:.8rem}.uppercase{text-transform:uppercase}#logo h4,.capitalize,h2{text-transform:capitalize}.flex{display:flex}.grid{display:grid}.grid-3{grid-template-columns:1fr auto auto}.flow-column{grid-auto-flow:column}.gap-0-5{gap:.5rem}.gap-1{gap:1rem}.gap-1-5{gap:1.5rem}.gap-2{gap:2rem}.gap-3{gap:3rem}.text-align-right{text-align:right}.align-start{align-items:flex-start}.align-center{align-items:center}.text-center{text-align:center}.justify-start{justify-content:start}.justify-center{justify-content:center}.justify-right{margin-left:auto}.align-self-center{align-self:center}.justify-self-center{justify-self:center}.justify-self-start{justify-self:start}.justify-self-end{justify-self:end}.direction-column{flex-direction:column}.space-between{justify-content:space-between}.w-100{width:100%}.color-0-8{color:rgba(var(--text-color),.8)}.weight-400{font-weight:400}.weight-500{font-weight:500}.ripple{position:absolute;border-radius:50%;transform:scale(0);background:rgba(var(--text-color),.16)}.interact{position:relative;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:transparent}.observe-empty-state:empty{display:none}.observe-empty-state:not(:empty)~.empty-state{display:none}#logo,main{display:grid}.icon{width:1.5rem;height:1.5rem;fill:rgba(var(--text-color),.9)}.button__icon{height:1.2rem;width:1.2rem}.button__icon--left{margin-right:.5rem}.button__icon--right{margin-left:.5rem}#confirmation_popup,#prompt_popup{flex-direction:column}#confirmation_popup h4,#prompt_popup h4{font-weight:500;margin-bottom:.5rem}#confirmation_popup sm-button,#prompt_popup sm-button{margin:0}#confirmation_popup .flex,#prompt_popup .flex{padding:0;margin-top:1rem}#confirmation_popup .flex sm-button:first-of-type,#prompt_popup .flex sm-button:first-of-type{margin-right:.6rem;margin-left:auto}h1,h2,h3,h4.h5{font-family:Poppins,sans-serif}h2{margin:3rem 0 1rem}main{height:100%;grid-template-rows:auto 1fr auto;grid-template-areas:"main-header" "." "side-nav"}#main_header{grid-area:main-header;padding:1rem 1.5rem;border-bottom:1px solid rgba(var(--text-color),.1)}#logo{align-items:center;width:100%;grid-template-columns:auto 1fr;gap:0 .5rem;margin-right:1rem}#logo h4{font-weight:600}#logo #main_logo{height:1.4rem;width:1.4rem;fill:rgba(var(--text-color),1);stroke:none}#side_nav{grid-area:side-nav}#side_nav h4{font-size:.9rem;letter-spacing:.08em;text-transform:uppercase;padding:1.5rem}.nav-list{display:flex;justify-content:space-evenly}.nav-list li{width:100%}.nav-list__item{display:flex;padding:.8rem 1.5rem;text-transform:capitalize}.nav-list__item--active{color:var(--accent-color)}.nav-list__item--active .icon{fill:var(--accent-color)}.right{max-height:100%;overflow-y:auto;padding:1.5rem}.right h1{margin-bottom:1.5rem}.page{display:flex;flex-direction:column;padding-bottom:3rem}@media screen and (max-width:640px){main{grid-template-rows:auto 1fr;grid-template-columns:1fr}.nav-list__item{flex-direction:column;justify-content:center;align-items:center;padding:.4rem}.nav-list__item span{font-size:.8rem;margin-top:.3rem}}@media screen and (min-width:640px){sm-popup{--width:32rem}main{grid-template-columns:14rem minmax(0,1fr);grid-template-rows:auto 1fr;grid-template-areas:"main-header main-header" "side-nav ."}.nav-list{flex-direction:column}.nav-list__item{align-items:center;justify-content:start}.nav-list__item--active{background:rgba(var(--text-color),.06)}.nav-list__item .icon{margin-right:.5rem}.right{display:grid;grid-template-columns:1fr 90% 1fr}.right>*{grid-column:2/3}.page__title{font-size:2.5rem}}@media (any-hover:hover){::-webkit-scrollbar{width:.5rem;height:.5rem}::-webkit-scrollbar-thumb{background:rgba(var(--text-color),.3);border-radius:1rem}::-webkit-scrollbar-thumb:hover{background:rgba(var(--text-color),.5)}.nav-list__item:hover{background:rgba(var(--text-color),.1);cursor:pointer}} \ No newline at end of file diff --git a/css/main.scss b/css/main.scss new file mode 100644 index 0000000..dacfd5b --- /dev/null +++ b/css/main.scss @@ -0,0 +1,407 @@ +*{ + padding: 0; + margin: 0; + box-sizing: border-box; + font-family: 'Roboto', sans-serif; +} +:root{ + font-size: clamp(1rem, 1.2vmax, 3rem); +} +html, body{ + height: 100%; + scroll-behavior: smooth; +} +body { + &, + *{ + --accent-color: #0D7377; + --text-color: 17, 17, 17; + --background-color: 255, 255, 255; + --danger-color: red; + } + color: rgba(var(--text-color), 1); + background: rgba(var(--background-color), 1); +} +body[data-theme='dark']{ + &, + *{ + --accent-color: #32E0C4; + --text-color: 240, 240, 240; + --text-color-light: 170, 170, 170; + --background-color: 10, 10, 10; + --danger-color: rgb(255, 106, 106); + } +} +p { + font-size: 0.8; + max-width: 65ch; + line-height: 1.7; + margin-bottom: 1.5rem; + color: rgba(var(--text-color), 0.8); + &:not(:last-of-type){ + margin-bottom: 1rem; + } +} +img{ + object-fit: cover; +} + +a{ + color: inherit; + text-decoration: none; + &:focus-visible{ + box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset; + } +} + +button{ + display: inline-flex; + border: none; + background-color: inherit; +} + +a:any-link:focus-visible{ + outline: rgba(var(--text-color), 1) 0.1rem solid; +} +sm-button{ + --border-radius: 0.3rem; +} +ul{ + list-style: none; +} +.flex{ + display: flex; +} +.grid{ + display: grid; +} +.hide{ + opacity: 0; + pointer-events: none; +} +.hide-completely{ + display: none !important; +} +.no-transformations{ + transform: none !important; +} +.overflow-ellipsis{ + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.breakable{ + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-word; + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} +.full-bleed{ + grid-column: 1/4; +} +.h1{ + font-size: 2.5rem; +} +.h2{ + font-size: 2rem; +} +.h3{ + font-size: 1.4rem; +} +.h4{ + font-size: 1rem; +} +.h5{ + font-size: 0.8rem; +} + +.uppercase{ + text-transform: uppercase; +} +.capitalize{ + text-transform: capitalize; +} +.flex{ + display: flex; +} +.grid{ + display: grid; +} +.grid-3{ + grid-template-columns: 1fr auto auto; +} +.flow-column{ + grid-auto-flow: column; +} +.gap-0-5{ + gap: 0.5rem; +} +.gap-1{ + gap: 1rem; +} +.gap-1-5{ + gap: 1.5rem; +} +.gap-2{ + gap: 2rem; +} +.gap-3{ + gap: 3rem; +} +.text-align-right{ + text-align: right; +} +.align-start{ + align-items: flex-start; +} +.align-center{ + align-items: center; +} +.text-center{ + text-align: center; +} +.justify-start{ + justify-content: start; +} +.justify-center{ + justify-content: center; +} +.justify-right{ + margin-left: auto; +} +.align-self-center{ + align-self: center; +} +.justify-self-center{ + justify-self: center; +} +.justify-self-start{ + justify-self: start; +} +.justify-self-end{ + justify-self: end; +} +.direction-column{ + flex-direction: column; +} +.space-between{ + justify-content: space-between; +} +.w-100{ + width: 100%; +} +.color-0-8{ + color: rgba(var(--text-color), 0.8); +} +.weight-400{ + font-weight: 400; +} +.weight-500{ + font-weight: 500; +} +.ripple{ + position: absolute; + border-radius: 50%; + transform: scale(0); + background: rgba(var(--text-color), 0.16); + pointer-events: none; +} +.interact{ + position: relative; + overflow: hidden; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} +.observe-empty-state:empty{ + display: none; +} +.observe-empty-state:not(:empty) ~ .empty-state{ + display: none; +} +.icon{ + width: 1.5rem; + height: 1.5rem; + fill: rgba(var(--text-color), 0.9); +} +.button__icon{ + height: 1.2rem; + width: 1.2rem; + &--left{ + margin-right: 0.5rem; + } + &--right{ + margin-left: 0.5rem; + } +} +#confirmation_popup, +#prompt_popup { + flex-direction: column; + h4 { + font-weight: 500; + margin-bottom: 0.5rem; + } + sm-button{ + margin: 0; + } + .flex { + padding: 0; + margin-top: 1rem; + sm-button:first-of-type { + margin-right: 0.6rem; + margin-left: auto; + } + } +} +h1,h2,h3,h4.h5{ + font-family: 'Poppins', sans-serif; +} +h2{ + margin: 3rem 0 1rem 0; + text-transform: capitalize; +} +main{ + display: grid; + height: 100%; + grid-template-rows: auto 1fr auto; + grid-template-areas: 'main-header' '.' 'side-nav'; + +} +#main_header{ + grid-area: main-header; + padding: 1rem 1.5rem; + border-bottom: 1px solid rgba(var(--text-color), .1); +} +#logo{ + display: grid; + align-items: center; + width: 100%; + grid-template-columns: auto 1fr; + gap: 0 0.5rem; + margin-right: 1rem; + h4{ + text-transform: capitalize; + font-size: 1rem; + font-weight: 600; + } + #main_logo{ + height: 1.4rem; + width: 1.4rem; + fill: rgba(var(--text-color), 1); + stroke: none; + } +} +.right{ + max-height: 100%; + overflow-y: auto; +} +#side_nav{ + grid-area: side-nav; + h4{ + font-size: 0.9rem; + letter-spacing: 0.08em; + text-transform: uppercase; + padding: 1.5rem; + } +} +.nav-list{ + list-style: none; + display: flex; + justify-content: space-evenly; + li{ + width: 100%; + } +} +.nav-list__item{ + display: flex; + padding: 0.8rem 1.5rem; + text-transform: capitalize; + &--active{ + color: var(--accent-color); + .icon{ + fill: var(--accent-color); + } + } +} +.right{ + padding: 1.5rem; + h1{ + margin-bottom: 1.5rem; + } +} +.page{ + display: flex; + flex-direction: column; + padding-bottom: 3rem; +} +@media screen and (max-width: 640px){ + main{ + grid-template-rows: auto 1fr; + grid-template-columns: 1fr; + } + .nav-list__item{ + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0.4rem; + span{ + font-size: 0.8rem; + margin-top: 0.3rem; + } + } +} +@media screen and (min-width: 640px){ + sm-popup{ + --width: 32rem; + } + main{ + grid-template-columns: 14rem minmax(0, 1fr); + grid-template-rows: auto 1fr; + grid-template-areas: 'main-header main-header' 'side-nav .'; + } + .nav-list{ + flex-direction: column; + } + .nav-list__item{ + align-items: center; + justify-content: start; + &--active{ + background: rgba(var(--text-color), .06); + } + .icon{ + margin-right: 0.5rem; + } + } + .right{ + display: grid; + grid-template-columns: 1fr 90% 1fr; + & > * { + grid-column: 2/3; + } + } + .page__title{ + font-size: 2.5rem; + } +} +@media (any-hover: hover){ + ::-webkit-scrollbar{ + width: 0.5rem; + height: 0.5rem; + } + + ::-webkit-scrollbar-thumb{ + background: rgba(var(--text-color), 0.3); + border-radius: 1rem; + &:hover{ + background: rgba(var(--text-color), 0.5); + } + } + .nav-list__item:hover{ + background: rgba(var(--text-color), .1); + cursor: pointer; + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..9a82188 --- /dev/null +++ b/index.html @@ -0,0 +1,11727 @@ + + + + + + + Mining Economic System + + + + + + + + + + +

+

+
+ Cancel + OK +
+
+ +

+

+ +
+ Cancel + OK +
+
+ + +
+
+

Sign in

+

Liking an article supports and encourages the creators.

+
+ + + Sign In + +
+

New here? Generate your FLO credentials below to continue

+ + Get FLO credentials + + + + + +
+
+
+ +
+

FLO credentials

+

You can use FLO credentials with RanchiMall Times and all RanchiMall FLO apps.

+
+
+
+
FLO ID
+ +
+
+
Private key
+ +
+
+ Sign in with these credentials + + Keep your private key secure and don't share with anyone. + Once lost there is no way to recover private key. + +
+
+ + +
+
+
+
Reset App Data?
+
+ Reset Data + +
+
My FLO ID
+ +
+ Sign out + +
+ Admin dashboard +
+
+ + +
+
+
Consumer ID
+ F8dfo322CKzPAyzoFJ6SefHZ9AwAujvKXi +
+
+
Consumer Amount
+ 1750 +
+
+
Consumer Tokens
+ 5250 +
+
+
From date
+ 2021-12-27 +
+
+
To date
+ 2021-12-28 +
+
+
Investor ID
+ FSAH5eajsfgXMGENZvpRTbuZTCBDGrTQQx +
+
+
Investor tokens
+ 5250 +
+
+
Property ID
+ F8BPRSTD2v6PHmKSJK3puPS7jpy3Mm1Ncq +
+
+
Producer tokens
+ 5250 +
+
+
Referrer ID
+ +
+
+
Referrer tokens
+ 0 +
+
+
Technology provider tokens
+ 1750 +
+
+
Transaction ID
+ z0baa7ff9b7255db4fa59592ab46504ca0dd58d9f9af67dc200c6063a04e338e6c +
+
+
+
+
+ + + + +
+ +
+
+

Dashboard

+

Details of overall tokens issued

+
+
+

Price per MINING

+

-

+
+
+

Total Amount issued

+

-

+
+
+

Total Tokens issued

+

-

+
+
+

Customer tokens

+

-

+
+
+

Service provider tokens

+

-

+
+
+
+ +
+

My Token Information

+

Information of user

+ +
+

Summary

+ + + + + + Trade + +
+ +
+
+

My Token Valuation

+

-

+
+
+

My Total FLOBNB

+

-

+
+
+

Price per FLOBNB

+

-

+
+
+

My FLOBNB on Exchange

+

-

+
+
+

My FLOBNB on Blockchain

+

-

+
+
+ +
+
+

+

Token Split details

+

+
+
+
+
+

My Consumer tokens

+

+ - +

+
+
+

My Property tokens

+

+ - +

+
+
+

My Investor tokens

+

+ - +

+
+
+

My Referrer tokens

+

+ - +

+
+
+
+
+

+

My Transactions

+

+
+
+
+
+
+ +
+

Subadmin

+

+

+ +

FLOBNB detail form

+

+ + + + + + + + + + + + + + + + Submit + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/components.min.js b/js/components.min.js new file mode 100644 index 0000000..6efcb63 --- /dev/null +++ b/js/components.min.js @@ -0,0 +1,1176 @@ +const smButton = document.createElement('template') +smButton.innerHTML = ` + +
+ +
`; +customElements.define('sm-button', + class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ + mode: 'open' + }).append(smButton.content.cloneNode(true)); + } + static get observedAttributes() { + return ['disabled']; + } + + get disabled() { + return this.hasAttribute('disabled'); + } + + set disabled(value) { + if (value) { + this.setAttribute('disabled', ''); + } else { + this.removeAttribute('disabled'); + } + } + focusIn() { + this.focus(); + } + + handleKeyDown(e) { + if (!this.hasAttribute('disabled') && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + this.click(); + } + } + + connectedCallback() { + if (!this.hasAttribute('disabled')) { + this.setAttribute('tabindex', '0'); + } + this.setAttribute('role', 'button'); + this.addEventListener('keydown', this.handleKeyDown); + } + attributeChangedCallback(name) { + if (name === 'disabled') { + if (this.hasAttribute('disabled')) { + this.removeAttribute('tabindex'); + } else { + this.setAttribute('tabindex', '0'); + } + this.setAttribute('aria-disabled', this.hasAttribute('disabled')); + } + } + }) + + const smForm = document.createElement('template'); + smForm.innerHTML = ` + +
+ +
+ `; + + customElements.define('sm-form', class extends HTMLElement { + constructor() { + super() + this.attachShadow({ + mode: 'open' + }).append(smForm.content.cloneNode(true)) + + this.form = this.shadowRoot.querySelector('form'); + this.formElements + this.requiredElements + this.submitButton + this.resetButton + this.allRequiredValid = false; + + this.debounce = this.debounce.bind(this) + this._checkValidity = this._checkValidity.bind(this) + this.handleKeydown = this.handleKeydown.bind(this) + this.reset = this.reset.bind(this) + this.elementsChanged = this.elementsChanged.bind(this) + } + debounce(callback, wait) { + let timeoutId = null; + return (...args) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback.apply(null, args); + }, wait); + }; + } + _checkValidity() { + this.allRequiredValid = this.requiredElements.every(elem => elem.isValid) + if (!this.submitButton) return; + if (this.allRequiredValid) { + this.submitButton.disabled = false; + } + else { + this.submitButton.disabled = true; + } + } + handleKeydown(e) { + if (e.key === 'Enter' && !e.target.tagName.includes('TEXTAREA')) { + if (this.allRequiredValid) { + if (this.submitButton) { + this.submitButton.click() + } + this.dispatchEvent(new CustomEvent('submit', { + bubbles: true, + composed: true, + })) + } + else { + this.requiredElements.find(elem => !elem.isValid).vibrate() + } + } + } + reset() { + this.formElements.forEach(elem => elem.reset()) + } + elementsChanged() { + this.formElements = [...this.querySelectorAll('sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio')] + this.requiredElements = this.formElements.filter(elem => elem.hasAttribute('required')); + this.submitButton = this.querySelector('[variant="primary"], [type="submit"]'); + this.resetButton = this.querySelector('[type="reset"]'); + if (this.resetButton) { + this.resetButton.addEventListener('click', this.reset); + } + this._checkValidity() + } + connectedCallback() { + const slot = this.shadowRoot.querySelector('slot') + slot.addEventListener('slotchange', this.elementsChanged) + this.addEventListener('input', this.debounce(this._checkValidity, 100)); + this.addEventListener('keydown', this.debounce(this.handleKeydown, 100)); + } + disconnectedCallback() { + this.removeEventListener('input', this.debounce(this._checkValidity, 100)); + this.removeEventListener('keydown', this.debounce(this.handleKeydown, 100)); + } + }) + + const smInput = document.createElement('template') + smInput.innerHTML = ` + +
+ + +
+ `; + customElements.define('sm-input', + class extends HTMLElement { + + constructor() { + super(); + this.attachShadow({ + mode: 'open' + }).append(smInput.content.cloneNode(true)); + + this.inputParent = this.shadowRoot.querySelector('.input'); + this.input = this.shadowRoot.querySelector('input'); + this.clearBtn = this.shadowRoot.querySelector('.clear'); + this.label = this.shadowRoot.querySelector('.label'); + this.feedbackText = this.shadowRoot.querySelector('.feedback-text'); + this.outerContainer = this.shadowRoot.querySelector('.outer-container'); + this._helperText = ''; + this._errorText = ''; + this.isRequired = false; + this.validationFunction = undefined; + this.reflectedAttributes = ['value', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step']; + + this.reset = this.reset.bind(this); + this.clear = this.clear.bind(this); + this.focusIn = this.focusIn.bind(this); + this.focusOut = this.focusOut.bind(this); + this.fireEvent = this.fireEvent.bind(this); + this.checkInput = this.checkInput.bind(this); + this.vibrate = this.vibrate.bind(this); + } + + static get observedAttributes() { + return ['value', 'placeholder', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step', 'helper-text', 'error-text', 'hiderequired']; + } + + get value() { + return this.input.value; + } + + set value(val) { + this.input.value = val; + this.checkInput(); + this.fireEvent(); + } + + get placeholder() { + return this.getAttribute('placeholder'); + } + + set placeholder(val) { + this.setAttribute('placeholder', val); + } + + get type() { + return this.getAttribute('type'); + } + + set type(val) { + this.setAttribute('type', val); + } + + get validity() { + return this.input.validity; + } + + get disabled() { + return this.hasAttribute('disabled'); + } + set disabled(value) { + if (value) + this.inputParent.classList.add('disabled'); + else + this.inputParent.classList.remove('disabled'); + } + get readOnly() { + return this.hasAttribute('readonly'); + } + set readOnly(value) { + if (value) { + this.setAttribute('readonly', ''); + } else { + this.removeAttribute('readonly'); + } + } + set customValidation(val) { + this.validationFunction = val; + } + set errorText(val) { + this._errorText = val; + } + set helperText(val) { + this._helperText = val; + } + get isValid() { + if (this.input.value !== '') { + const _isValid = this.input.checkValidity(); + let _customValid = true; + if (this.validationFunction) { + _customValid = Boolean(this.validationFunction(this.input.value)); + } + if (_isValid && _customValid) { + this.feedbackText.classList.remove('error'); + this.feedbackText.classList.add('success'); + this.feedbackText.textContent = ''; + } else { + if (this._errorText) { + this.feedbackText.classList.add('error'); + this.feedbackText.classList.remove('success'); + this.feedbackText.innerHTML = ` + + ${this._errorText} + `; + } + } + return (_isValid && _customValid); + } + } + reset() { + this.value = ''; + } + clear() { + this.value = ''; + this.input.focus(); + } + + focusIn() { + this.input.focus(); + } + + focusOut() { + this.input.blur(); + } + + fireEvent() { + let event = new Event('input', { + bubbles: true, + cancelable: true, + composed: true + }); + this.dispatchEvent(event); + } + + checkInput(e) { + if (!this.hasAttribute('readonly')) { + if (this.input.value.trim() !== '') { + this.clearBtn.classList.remove('hide'); + } else { + this.clearBtn.classList.add('hide'); + } + } + if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder').trim() === '') return; + if (this.input.value !== '') { + if (this.animate) + this.inputParent.classList.add('animate-label'); + else + this.label.classList.add('hide'); + } else { + if (this.animate) + this.inputParent.classList.remove('animate-label'); + else + this.label.classList.remove('hide'); + this.feedbackText.textContent = ''; + } + } + vibrate() { + this.outerContainer.animate([ + { transform: 'translateX(-1rem)' }, + { transform: 'translateX(1rem)' }, + { transform: 'translateX(-0.5rem)' }, + { transform: 'translateX(0.5rem)' }, + { transform: 'translateX(0)' }, + ], { + duration: 300, + easing: 'ease' + }); + } + + + connectedCallback() { + this.animate = this.hasAttribute('animate'); + this.setAttribute('role', 'textbox'); + this.input.addEventListener('input', this.checkInput); + this.clearBtn.addEventListener('click', this.clear); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue !== newValue) { + if (this.reflectedAttributes.includes(name)) { + if (this.hasAttribute(name)) { + this.input.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : ''); + } + else { + this.input.removeAttribute(name); + } + } + if (name === 'placeholder') { + this.label.textContent = newValue; + this.setAttribute('aria-label', newValue); + } + else if (this.hasAttribute('value')) { + this.checkInput(); + } + else if (name === 'type') { + if (this.hasAttribute('type') && this.getAttribute('type') === 'number') { + this.input.setAttribute('inputmode', 'numeric'); + } + } + else if (name === 'helper-text') { + this._helperText = this.getAttribute('helper-text'); + } + else if (name === 'error-text') { + this._errorText = this.getAttribute('error-text'); + } + else if (name === 'required') { + this.isRequired = this.hasAttribute('required'); + if (this.isRequired) { + this.setAttribute('aria-required', 'true'); + } + else { + this.setAttribute('aria-required', 'false'); + } + } + else if (name === 'readonly') { + if (this.hasAttribute('readonly')) { + this.inputParent.classList.add('readonly'); + } else { + this.inputParent.classList.remove('readonly'); + } + } + else if (name === 'disabled') { + if (this.hasAttribute('disabled')) { + this.inputParent.classList.add('disabled'); + } + else { + this.inputParent.classList.remove('disabled'); + } + } + } + } + disconnectedCallback() { + this.input.removeEventListener('input', this.checkInput); + this.clearBtn.removeEventListener('click', this.clear); + } + }) + +const hamburgerMenu=document.createElement("template");hamburgerMenu.innerHTML='\n\n
\n\n';class HamburgerMenu extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(hamburgerMenu.content.cloneNode(!0)),this.resumeScrolling=this.resumeScrolling.bind(this),this.open=this.open.bind(this),this.close=this.close.bind(this),this.sideNav=this.shadowRoot.querySelector(".side-nav"),this.backdrop=this.shadowRoot.querySelector(".backdrop"),this.isOpen=!1,this.animeOptions={duration:300,easing:"ease"}}static get observedAttributes(){return["open"]}resumeScrolling(){const n=document.body.style.top;window.scrollTo(0,-1*parseInt(n||"0")),setTimeout(()=>{document.body.style.overflow="auto",document.body.style.top="initial"},300)}open(){this.isOpen||(document.body.style.overflow="hidden",document.body.style.top=`-${window.scrollY}px`,this.classList.remove("hide"),this.sideNav.classList.add("reveal"),this.backdrop.classList.remove("hide"),this.backdrop.animate([{opacity:0},{opacity:1}],this.animeOptions).onfinish=(()=>{this.isOpen=!0,this.setAttribute("open","")}))}close(){this.isOpen&&(this.sideNav.classList.remove("reveal"),this.backdrop.animate([{opacity:1},{opacity:0}],this.animeOptions).onfinish=(()=>{this.backdrop.classList.add("hide"),this.classList.add("hide"),this.isOpen=!1,this.removeAttribute("open")}))}connectedCallback(){this.backdrop.addEventListener("click",this.close);const n=new ResizeObserver(n=>{window.innerWidth<640&&this.isOpen?this.classList.remove("hide"):this.classList.add("hide"),window.innerWidth>640&&this.classList.remove("hide")});n.observe(this)}disconnectedCallback(){this.backdrop.removeEventListener("click",this.close)}attributeChangedCallback(n,e,t){"open"===n&&this.hasAttribute("open")&&this.open()}}window.customElements.define("hamburger-menu",HamburgerMenu); +const smMenu=document.createElement("template");smMenu.innerHTML='\n\n
\n \n
\n \n
\n
',customElements.define("sm-menu",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smMenu.content.cloneNode(!0)),this.isOpen=!1,this.availableOptions,this.containerDimensions,this.animOptions={duration:200,easing:"ease"},this.optionList=this.shadowRoot.querySelector(".options"),this.menu=this.shadowRoot.querySelector(".menu"),this.icon=this.shadowRoot.querySelector(".icon"),this.expand=this.expand.bind(this),this.collapse=this.collapse.bind(this),this.toggle=this.toggle.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleClickoutSide=this.handleClickoutSide.bind(this)}static get observedAttributes(){return["value"]}get value(){return this.getAttribute("value")}set value(n){this.setAttribute("value",n)}expand(){this.isOpen||(this.optionList.classList.remove("hide"),this.optionList.animate([{transform:window.innerWidth<640?"translateY(1.5rem)":"translateY(-1rem)",opacity:"0"},{transform:"none",opacity:"1"}],this.animOptions).onfinish=(()=>{this.isOpen=!0,this.icon.classList.add("focused")}))}collapse(){this.isOpen&&(this.optionList.animate([{transform:"none",opacity:"1"},{transform:window.innerWidth<640?"translateY(1.5rem)":"translateY(-1rem)",opacity:"0"}],this.animOptions).onfinish=(()=>{this.isOpen=!1,this.icon.classList.remove("focused"),this.optionList.classList.add("hide")}))}toggle(){this.isOpen?this.collapse():this.expand()}handleKeyDown(n){n.target===this?"ArrowDown"===n.code?(n.preventDefault(),this.availableOptions[0].focus()):"Enter"!==n.code&&"Space"!==n.code||(n.preventDefault(),this.toggle()):"ArrowUp"===n.code?(n.preventDefault(),document.activeElement.previousElementSibling?document.activeElement.previousElementSibling.focus():this.availableOptions[this.availableOptions.length-1].focus()):"ArrowDown"===n.code?(n.preventDefault(),document.activeElement.nextElementSibling?document.activeElement.nextElementSibling.focus():this.availableOptions[0].focus()):"Enter"!==n.code&&"Space"!==n.code||(n.preventDefault(),n.target.click())}handleClickoutSide(n){this.contains(n.target)||2===n.button||this.collapse()}connectedCallback(){this.setAttribute("role","listbox"),this.setAttribute("aria-label","dropdown menu");const n=this.shadowRoot.querySelector(".options slot");n.addEventListener("slotchange",n=>{this.availableOptions=n.target.assignedElements(),this.containerDimensions=this.optionList.getBoundingClientRect()}),this.addEventListener("click",this.toggle),this.addEventListener("keydown",this.handleKeyDown),document.addEventListener("mousedown",this.handleClickoutSide)}disconnectedCallback(){this.removeEventListener("click",this.toggle),this.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("mousedown",this.handleClickoutSide)}});const menuOption=document.createElement("template");menuOption.innerHTML='\n\n
\n \n
',customElements.define("menu-option",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(menuOption.content.cloneNode(!0))}connectedCallback(){this.setAttribute("role","option"),this.addEventListener("keyup",n=>{"Enter"!==n.code&&"Space"!==n.code||(n.preventDefault(),this.click())})}}); +const smNotifications=document.createElement("template");smNotifications.innerHTML='\n\n
\n',customElements.define("sm-notifications",class extends HTMLElement{constructor(){super(),this.shadow=this.attachShadow({mode:"open"}).append(smNotifications.content.cloneNode(!0)),this.notificationPanel=this.shadowRoot.querySelector(".notification-panel"),this.animationOptions={duration:300,fill:"forwards",easing:"cubic-bezier(0.175, 0.885, 0.32, 1.275)"},this.push=this.push.bind(this),this.createNotification=this.createNotification.bind(this),this.removeNotification=this.removeNotification.bind(this),this.clearAll=this.clearAll.bind(this)}randString(n){let t="";const i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let o=0;o${o}\n

${n}

\n `,i&&(e.classList.add("pinned"),a+='\n \n '),e.innerHTML=a,e}push(n,t={}){const i=this.createNotification(n,t);return this.notificationPanel.append(i),i.animate([{transform:"translateY(1rem)",opacity:"0"},{transform:"none",opacity:"1"}],this.animationOptions),i.id}removeNotification(n){n.animate([{transform:"none",opacity:"1"},{transform:"translateY(0.5rem)",opacity:"0"}],this.animationOptions).onfinish=(()=>{n.remove()})}clearAll(){Array.from(this.notificationPanel.children).forEach(n=>{this.removeNotification(n)})}connectedCallback(){this.notificationPanel.addEventListener("click",n=>{n.target.closest(".close")&&this.removeNotification(n.target.closest(".notification"))});const n=new MutationObserver(n=>{n.forEach(n=>{"childList"===n.type&&n.addedNodes.length&&!n.addedNodes[0].classList.contains("pinned")&&setTimeout(()=>{this.removeNotification(n.addedNodes[0])},5e3)})});n.observe(this.notificationPanel,{childList:!0})}}); +const themeToggle=document.createElement("template");themeToggle.innerHTML='\n \n \n';class ThemeToggle extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(themeToggle.content.cloneNode(!0)),this.isChecked=!1,this.hasTheme="light",this.toggleState=this.toggleState.bind(this),this.fireEvent=this.fireEvent.bind(this),this.handleThemeChange=this.handleThemeChange.bind(this)}static get observedAttributes(){return["checked"]}daylight(){this.hasTheme="light",document.body.dataset.theme="light",this.setAttribute("aria-checked","false")}nightlight(){this.hasTheme="dark",document.body.dataset.theme="dark",this.setAttribute("aria-checked","true")}toggleState(){this.toggleAttribute("checked"),this.fireEvent()}handleKeyDown(e){"Space"===e.code&&this.toggleState()}handleThemeChange(e){e.detail.theme!==this.hasTheme&&("dark"===e.detail.theme?this.setAttribute("checked",""):this.removeAttribute("checked"))}fireEvent(){this.dispatchEvent(new CustomEvent("themechange",{bubbles:!0,composed:!0,detail:{theme:this.hasTheme}}))}connectedCallback(){this.setAttribute("role","switch"),this.setAttribute("aria-label","theme toggle"),"dark"===localStorage.theme?(this.nightlight(),this.setAttribute("checked","")):"light"===localStorage.theme?(this.daylight(),this.removeAttribute("checked")):window.matchMedia("(prefers-color-scheme: dark)").matches?(this.nightlight(),this.setAttribute("checked","")):(this.daylight(),this.removeAttribute("checked")),this.addEventListener("click",this.toggleState),this.addEventListener("keydown",this.handleKeyDown),document.addEventListener("themechange",this.handleThemeChange)}disconnectedCallback(){this.removeEventListener("click",this.toggleState),this.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("themechange",this.handleThemeChange)}attributeChangedCallback(e,t,n){"checked"===e&&(this.hasAttribute("checked")?(this.nightlight(),localStorage.setItem("theme","dark")):(this.daylight(),localStorage.setItem("theme","light")))}}window.customElements.define("theme-toggle",ThemeToggle); +const fileInput=document.createElement("template");fileInput.innerHTML='\n \t\n\t\n \t\n',customElements.define("file-input",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(fileInput.content.cloneNode(!0)),this.input=this.shadowRoot.querySelector("input"),this.fileInput=this.shadowRoot.querySelector(".file-input"),this.filesPreviewWrapper=this.shadowRoot.querySelector(".files-preview-wrapper"),this.reflectedAttributes=["accept","multiple","capture","type"],this.reset=this.reset.bind(this),this.formatBytes=this.formatBytes.bind(this),this.createFilePreview=this.createFilePreview.bind(this),this.handleChange=this.handleChange.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this)}static get observedAttributes(){return["accept","multiple","capture","type"]}get files(){return this.input.files}set accept(t){this.setAttribute("accept",t)}set multiple(t){t?this.setAttribute("multiple",""):this.removeAttribute("multiple")}set capture(t){this.setAttribute("capture",t)}set value(t){this.input.value=t}get isValid(){return""!==this.input.value}reset(){this.input.value="",this.filesPreviewWrapper.innerHTML=""}formatBytes(t,e=2){if(0===t)return"0 Bytes";const n=0>e?0:e,i=Math.floor(Math.log(t)/Math.log(1024));return parseFloat((t/Math.pow(1024,i)).toFixed(n))+" "+["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"][i]}createFilePreview(t){const e=document.createElement("li"),{name:n,size:i}=t;return e.className="file-preview",e.innerHTML=`\n\t\t\t
${n}
\n
${this.formatBytes(i)}
\n\t\t`,e}handleChange(t){this.filesPreviewWrapper.innerHTML="";const e=document.createDocumentFragment();Array.from(t.target.files).forEach(t=>{e.append(this.createFilePreview(t))}),this.filesPreviewWrapper.append(e)}handleKeyDown(t){"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),this.input.click())}connectedCallback(){this.setAttribute("role","button"),this.setAttribute("aria-label","File upload"),this.input.addEventListener("change",this.handleChange),this.fileInput.addEventListener("keydown",this.handleKeyDown)}attributeChangedCallback(t){this.reflectedAttributes.includes(t)&&(this.hasAttribute(t)?this.input.setAttribute(t,this.getAttribute(t)?this.getAttribute(t):""):this.input.removeAttribute(t))}disconnectedCallback(){this.input.removeEventListener("change",this.handleChange),this.fileInput.removeEventListener("keydown",this.handleKeyDown)}}); + +const smCarousel=document.createElement("template");smCarousel.innerHTML='\n\n\n',customElements.define("sm-carousel",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smCarousel.content.cloneNode(!0)),this.isAutoPlaying=!1,this.autoPlayInterval=5e3,this.autoPlayTimeout,this.initialTimeout,this.activeSlideNum=0,this.carouselItems,this.indicators,this.showIndicator=!1,this.carousel=this.shadowRoot.querySelector(".carousel"),this.carouselContainer=this.shadowRoot.querySelector(".carousel-container"),this.carouselSlot=this.shadowRoot.querySelector("slot"),this.navButtonRight=this.shadowRoot.querySelector(".carousel__button--right"),this.navButtonLeft=this.shadowRoot.querySelector(".carousel__button--left"),this.indicatorsContainer=this.shadowRoot.querySelector(".indicators"),this.scrollLeft=this.scrollLeft.bind(this),this.scrollRight=this.scrollRight.bind(this),this.handleIndicatorClick=this.handleIndicatorClick.bind(this),this.showSlide=this.showSlide.bind(this),this.nextSlide=this.nextSlide.bind(this),this.autoPlay=this.autoPlay.bind(this),this.startAutoPlay=this.startAutoPlay.bind(this),this.stopAutoPlay=this.stopAutoPlay.bind(this)}static get observedAttributes(){return["indicator","autoplay","interval"]}scrollLeft(){this.carousel.scrollBy({left:-this.scrollDistance,behavior:"smooth"})}scrollRight(){this.carousel.scrollBy({left:this.scrollDistance,behavior:"smooth"})}showSlide(t){this.carousel.scrollTo({left:this.carouselItems[t].getBoundingClientRect().left-this.carousel.getBoundingClientRect().left+this.carousel.scrollLeft,behavior:"smooth"})}nextSlide(){if(!this.carouselItems)return;let t=this.activeSlideNum+1{this.autoPlay()},this.autoPlayInterval))}startAutoPlay(){this.setAttribute("autoplay","")}stopAutoPlay(){this.removeAttribute("autoplay")}createIndicator(t){let n=document.createElement("div");return n.classList.add("indicator"),n.dataset.rank=t,n}handleIndicatorClick(t){if(t.target.closest(".indicator")){const n=parseInt(t.target.closest(".indicator").dataset.rank);this.activeSlideNum!==n&&this.showSlide(n)}}handleKeyDown(t){"ArrowLeft"===t.code?this.scrollRight():"ArrowRight"===t.code&&this.scrollRight()}connectedCallback(){let t=document.createDocumentFragment();this.carouselSlot.addEventListener("slotchange",n=>{this.carouselItems=this.carouselSlot.assignedElements(),this.carouselItems.forEach(t=>i.observe(t)),this.carouselItems.length>0?(o.observe(this.carouselItems[0]),e.observe(this.carouselItems[this.carouselItems.length-1])):(navButtonLeft.classList.add("hide"),navButtonRight.classList.add("hide"),o.disconnect(),e.disconnect()),this.showIndicator&&(this.indicatorsContainer.innerHTML="",this.carouselItems.forEach((n,i)=>{t.append(this.createIndicator(i)),n.dataset.rank=i}),this.indicatorsContainer.append(t),this.indicators=this.indicatorsContainer.children)});const n={threshold:.9,root:this},i=new IntersectionObserver(t=>{t.forEach(t=>{if(this.showIndicator){const n=parseInt(t.target.dataset.rank);t.isIntersecting?(this.indicators[n].classList.add("active"),this.activeSlideNum=n):this.indicators[n].classList.remove("active")}})},n),o=new IntersectionObserver(t=>{t.forEach(t=>{t.isIntersecting?this.navButtonLeft.classList.add("hide"):this.navButtonLeft.classList.remove("hide")})},n),e=new IntersectionObserver(t=>{t.forEach(t=>{t.isIntersecting?this.navButtonRight.classList.add("hide"):this.navButtonRight.classList.remove("hide")})},n),s=new ResizeObserver(t=>{t.forEach(t=>{if(t.contentBoxSize){const n=Array.isArray(t.contentBoxSize)?t.contentBoxSize[0]:t.contentBoxSize;this.scrollDistance=.6*n.inlineSize}else this.scrollDistance=.6*t.contentRect.width})});s.observe(this),this.addEventListener("keydown",this.handleKeyDown),this.navButtonRight.addEventListener("click",this.scrollRight),this.navButtonLeft.addEventListener("click",this.scrollLeft),this.indicatorsContainer.addEventListener("click",this.handleIndicatorClick)}attributeChangedCallback(t,n,i){n!==i&&("indicator"===t&&(this.showIndicator=this.hasAttribute("indicator")),"autoplay"===t&&(this.hasAttribute("autoplay")?this.initialTimeout=setTimeout(()=>{this.isAutoPlaying=!0,this.autoPlay()},this.autoPlayInterval):(this.isAutoPlaying=!1,clearTimeout(this.autoPlayTimeout),clearTimeout(this.initialTimeout))),"interval"===t&&(this.hasAttribute("interval")&&""!==this.getAttribute("interval").trim()?this.autoPlayInterval=Math.abs(parseInt(this.getAttribute("interval").trim())):this.autoPlayInterval=5e3))}disconnectedCallback(){this.navButtonRight.removeEventListener("click",this.scrollRight),this.navButtonLeft.removeEventListener("click",this.scrollLeft),this.indicatorsContainer.removeEventListener("click",this.handleIndicatorClick)}}); +const smTabHeader=document.createElement("template");smTabHeader.innerHTML='\n\n
\n
\n \n
\n
\n
\n',customElements.define("sm-tab-header",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smTabHeader.content.cloneNode(!0)),this.prevTab,this.allTabs,this.activeTab,this.indicator=this.shadowRoot.querySelector(".indicator"),this.tabSlot=this.shadowRoot.querySelector("slot"),this.tabHeader=this.shadowRoot.querySelector(".tab-header"),this.changeTab=this.changeTab.bind(this),this.handleClick=this.handleClick.bind(this),this.handlePanelChange=this.handlePanelChange.bind(this)}fireEvent(t){this.dispatchEvent(new CustomEvent(`switchedtab${this.target}`,{bubbles:!0,detail:{index:parseInt(t)}}))}moveIndiactor(t){this.indicator.setAttribute("style",`width: ${t.width}px; transform: translateX(${t.left-this.tabHeader.getBoundingClientRect().left+this.tabHeader.scrollLeft}px)`)}changeTab(t){t!==this.prevTab&&t.closest("sm-tab")&&(this.prevTab&&this.prevTab.classList.remove("active"),t.classList.add("active"),t.scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"}),this.moveIndiactor(t.getBoundingClientRect()),this.prevTab=t,this.activeTab=t)}handleClick(t){t.target.closest("sm-tab")&&(this.changeTab(t.target),this.fireEvent(t.target.dataset.index))}handlePanelChange(t){this.changeTab(this.allTabs[t.detail.index])}connectedCallback(){if(!this.hasAttribute("target")||""===this.getAttribute("target").value)return;this.target=this.getAttribute("target"),this.tabSlot.addEventListener("slotchange",()=>{this.allTabs=this.tabSlot.assignedElements(),this.allTabs.forEach((t,n)=>{t.dataset.index=n})}),this.addEventListener("click",this.handleClick),document.addEventListener(`switchedpanel${this.target}`,this.handlePanelChange);let t=new ResizeObserver(t=>{t.forEach(t=>{if(this.prevTab){let t=this.activeTab.getBoundingClientRect();this.moveIndiactor(t)}})});t.observe(this);let n=new IntersectionObserver(t=>{t.forEach(t=>{if(t.isIntersecting)if(this.indicator.style.transition="none",this.activeTab){let t=this.activeTab.getBoundingClientRect();this.moveIndiactor(t)}else{this.allTabs[0].classList.add("active");let t=this.allTabs[0].getBoundingClientRect();this.moveIndiactor(t),this.fireEvent(0),this.prevTab=this.tabSlot.assignedElements()[0],this.activeTab=this.prevTab}})},{threshold:1});n.observe(this)}disconnectedCallback(){this.removeEventListener("click",this.handleClick),document.removeEventListener(`switchedpanel${this.target}`,this.handlePanelChange)}});const smTab=document.createElement("template");smTab.innerHTML='\n\n
\n\n
\n',customElements.define("sm-tab",class extends HTMLElement{constructor(){super(),this.shadow=this.attachShadow({mode:"open"}).append(smTab.content.cloneNode(!0))}});const smTabPanels=document.createElement("template");smTabPanels.innerHTML='\n\n
\n Nothing to see here.\n
\n',customElements.define("sm-tab-panels",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smTabPanels.content.cloneNode(!0)),this.isTransitioning=!1,this.panelContainer=this.shadowRoot.querySelector(".panel-container"),this.panelSlot=this.shadowRoot.querySelector("slot"),this.handleTabChange=this.handleTabChange.bind(this)}handleTabChange(t){this.isTransitioning=!0,this.panelContainer.scrollTo({left:this.allPanels[t.detail.index].getBoundingClientRect().left-this.panelContainer.getBoundingClientRect().left+this.panelContainer.scrollLeft,behavior:"smooth"}),setTimeout(()=>{this.isTransitioning=!1},300)}fireEvent(t){this.dispatchEvent(new CustomEvent(`switchedpanel${this.id}`,{bubbles:!0,detail:{index:parseInt(t)}}))}connectedCallback(){this.panelSlot.addEventListener("slotchange",()=>{this.allPanels=this.panelSlot.assignedElements(),this.allPanels.forEach((n,e)=>{n.dataset.index=e,t.observe(n)})}),document.addEventListener(`switchedtab${this.id}`,this.handleTabChange);const t=new IntersectionObserver(t=>{t.forEach(t=>{!this.isTransitioning&&t.isIntersecting&&this.fireEvent(t.target.dataset.index)})},{threshold:.6})}disconnectedCallback(){intersectionObserver.disconnect(),document.removeEventListener(`switchedtab${this.id}`,this.handleTabChange)}}); +const smCheckbox=document.createElement("template");smCheckbox.innerHTML='\n\n',customElements.define("sm-checkbox",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smCheckbox.content.cloneNode(!0)),this.defaultState,this.checkbox=this.shadowRoot.querySelector(".checkbox"),this.reset=this.reset.bind(this),this.dispatch=this.dispatch.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleClick=this.handleClick.bind(this)}static get observedAttributes(){return["value","disabled","checked"]}get disabled(){return this.hasAttribute("disabled")}set disabled(e){e?this.setAttribute("disabled",""):this.removeAttribute("disabled")}get checked(){return this.hasAttribute("checked")}set checked(e){e?this.setAttribute("checked",""):this.removeAttribute("checked")}set value(e){this.setAttribute("value",e)}get value(){return this.getAttribute("value")}focusIn(){this.focus()}reset(){this.value=this.defaultState}dispatch(){this.dispatchEvent(new CustomEvent("change",{bubbles:!0,composed:!0}))}handleKeyDown(e){" "===e.key&&(e.preventDefault(),this.click())}handleClick(e){this.toggleAttribute("checked")}connectedCallback(){this.hasAttribute("disabled")||this.setAttribute("tabindex","0"),this.setAttribute("role","checkbox"),this.defaultState=this.hasAttribute("checked"),this.hasAttribute("checked")||this.setAttribute("aria-checked","false"),this.addEventListener("keydown",this.handleKeyDown),this.addEventListener("click",this.handleClick)}attributeChangedCallback(e,t,n){t!==n&&("checked"===e?(this.setAttribute("aria-checked",this.hasAttribute("checked")),this.dispatch()):"disabled"===e&&(this.hasAttribute("disabled")?this.removeAttribute("tabindex"):this.setAttribute("tabindex","0")))}disconnectedCallback(){this.removeEventListener("keydown",this.handleKeyDown),this.removeEventListener("change",this.handleClick)}}); +const smCopy=document.createElement("template");smCopy.innerHTML='\n\n
\n

\n \n
\n',customElements.define("sm-copy",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}).append(smCopy.content.cloneNode(!0)),this.copyContent=this.shadowRoot.querySelector(".copy-content"),this.copyButton=this.shadowRoot.querySelector(".copy-button"),this.copy=this.copy.bind(this)}static get observedAttributes(){return["value"]}set value(n){this.setAttribute("value",n)}get value(){return this.getAttribute("value")}fireEvent(){this.dispatchEvent(new CustomEvent("copy",{composed:!0,bubbles:!0,cancelable:!0}))}copy(){navigator.clipboard.writeText(this.copyContent.textContent).then(n=>this.fireEvent()).catch(n=>console.error(n))}connectedCallback(){this.copyButton.addEventListener("click",this.copy)}attributeChangedCallback(n,t,o){"value"===n&&(this.copyContent.textContent=o)}disconnectedCallback(){this.copyButton.removeEventListener("click",this.copy)}}); + +class Stack { + constructor() { + this.items = []; + } + push(element) { + this.items.push(element); + } + pop() { + if (this.items.length == 0) + return "Underflow"; + return this.items.pop(); + } + peek() { + return this.items[this.items.length - 1]; + } +} +const popupStack = new Stack(); + +const smPopup = document.createElement('template'); +smPopup.innerHTML = ` + + +`; +customElements.define('sm-popup', class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ + mode: 'open' + }).append(smPopup.content.cloneNode(true)); + + this.allowClosing = false; + this.isOpen = false; + this.pinned = false; + this.offset = 0; + this.touchStartY = 0; + this.touchEndY = 0; + this.touchStartTime = 0; + this.touchEndTime = 0; + this.touchEndAnimation = undefined; + this.focusable + this.autoFocus + this.mutationObserver + + this.popupContainer = this.shadowRoot.querySelector('.popup-container'); + this.backdrop = this.shadowRoot.querySelector('.background'); + this.popup = this.shadowRoot.querySelector('.popup'); + this.popupBodySlot = this.shadowRoot.querySelector('.popup-body slot'); + this.popupHeader = this.shadowRoot.querySelector('.popup-top'); + + this.resumeScrolling = this.resumeScrolling.bind(this); + this.setStateOpen = this.setStateOpen.bind(this); + this.show = this.show.bind(this); + this.hide = this.hide.bind(this); + this.handleTouchStart = this.handleTouchStart.bind(this); + this.handleTouchMove = this.handleTouchMove.bind(this); + this.handleTouchEnd = this.handleTouchEnd.bind(this); + this.detectFocus = this.detectFocus.bind(this); + } + + static get observedAttributes() { + return ['open']; + } + + get open() { + return this.isOpen; + } + + animateTo(element, keyframes, options) { + const anime = element.animate(keyframes, { ...options, fill: 'both' }) + anime.finished.then(() => { + anime.commitStyles() + anime.cancel() + }) + return anime + } + + resumeScrolling() { + const scrollY = document.body.style.top; + window.scrollTo(0, parseInt(scrollY || '0') * -1); + document.body.style.overflow = ''; + document.body.style.top = 'initial'; + } + + setStateOpen() { + if (!this.isOpen || this.offset) { + const animOptions = { + duration: 300, + easing: 'ease' + } + const initialAnimation = (window.innerWidth > 640) ? 'scale(1.1)' : `translateY(${this.offset ? `${this.offset}px` : '100%'})` + this.animateTo(this.popup, [ + { + opacity: this.offset ? 1 : 0, + transform: initialAnimation + }, + { + opacity: 1, + transform: 'none' + }, + ], animOptions) + + } + } + + show(options = {}) { + const { pinned = false } = options; + if (!this.isOpen) { + const animOptions = { + duration: 300, + easing: 'ease' + } + popupStack.push({ + popup: this, + permission: pinned + }); + if (popupStack.items.length > 1) { + this.animateTo(popupStack.items[popupStack.items.length - 2].popup.shadowRoot.querySelector('.popup'), [ + { transform: 'none' }, + { transform: (window.innerWidth > 640) ? 'scale(0.95)' : 'translateY(-1.5rem)' }, + ], animOptions) + } + this.popupContainer.classList.remove('hide'); + if (!this.offset) + this.backdrop.animate([ + { opacity: 0 }, + { opacity: 1 }, + ], animOptions) + this.setStateOpen() + this.dispatchEvent( + new CustomEvent("popupopened", { + bubbles: true, + detail: { + popup: this, + } + }) + ); + this.pinned = pinned; + this.isOpen = true; + document.body.style.overflow = 'hidden'; + document.body.style.top = `-${window.scrollY}px`; + const elementToFocus = this.autoFocus || this.focusable[0]; + elementToFocus.tagName.includes('SM-') ? elementToFocus.focusIn() : elementToFocus.focus(); + if (!this.hasAttribute('open')) + this.setAttribute('open', ''); + } + } + hide() { + const animOptions = { + duration: 150, + easing: 'ease' + } + this.backdrop.animate([ + { opacity: 1 }, + { opacity: 0 } + ], animOptions) + this.animateTo(this.popup, [ + { + opacity: 1, + transform: (window.innerWidth > 640) ? 'none' : `translateY(${this.offset ? `${this.offset}px` : '0'})` + }, + { + opacity: 0, + transform: (window.innerWidth > 640) ? 'scale(1.1)' : 'translateY(100%)' + }, + ], animOptions).finished + .finally(() => { + this.popupContainer.classList.add('hide'); + this.popup.style = '' + this.removeAttribute('open'); + + if (this.forms.length) { + this.forms.forEach(form => form.reset()); + } + this.dispatchEvent( + new CustomEvent("popupclosed", { + bubbles: true, + detail: { + popup: this, + } + }) + ); + this.isOpen = false; + }) + popupStack.pop(); + if (popupStack.items.length) { + this.animateTo(popupStack.items[popupStack.items.length - 1].popup.shadowRoot.querySelector('.popup'), [ + { transform: (window.innerWidth > 640) ? 'scale(0.95)' : 'translateY(-1.5rem)' }, + { transform: 'none' }, + ], animOptions) + + } else { + this.resumeScrolling(); + } + } + + handleTouchStart(e) { + this.offset = 0 + this.popupHeader.addEventListener('touchmove', this.handleTouchMove, { passive: true }); + this.popupHeader.addEventListener('touchend', this.handleTouchEnd, { passive: true }); + this.touchStartY = e.changedTouches[0].clientY; + this.touchStartTime = e.timeStamp; + } + + handleTouchMove(e) { + if (this.touchStartY < e.changedTouches[0].clientY) { + this.offset = e.changedTouches[0].clientY - this.touchStartY; + this.touchEndAnimation = window.requestAnimationFrame(() => { + this.popup.style.transform = `translateY(${this.offset}px)`; + }); + } + } + + handleTouchEnd(e) { + this.touchEndTime = e.timeStamp; + cancelAnimationFrame(this.touchEndAnimation); + this.touchEndY = e.changedTouches[0].clientY; + this.threshold = this.popup.getBoundingClientRect().height * 0.3; + if (this.touchEndTime - this.touchStartTime > 200) { + if (this.touchEndY - this.touchStartY > this.threshold) { + if (this.pinned) { + this.setStateOpen(); + return; + } else + this.hide(); + } else { + this.setStateOpen(); + } + } else { + if (this.touchEndY > this.touchStartY) + if (this.pinned) { + this.setStateOpen(); + return; + } + else + this.hide(); + } + this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true }); + this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true }); + } + + + detectFocus(e) { + if (e.key === 'Tab') { + const lastElement = this.focusable[this.focusable.length - 1]; + const firstElement = this.focusable[0]; + if (e.shiftKey && document.activeElement === firstElement) { + e.preventDefault(); + lastElement.tagName.includes('SM-') ? lastElement.focusIn() : lastElement.focus(); + } else if (!e.shiftKey && document.activeElement === lastElement) { + e.preventDefault(); + firstElement.tagName.includes('SM-') ? firstElement.focusIn() : firstElement.focus(); + } + } + } + + updateFocusableList() { + this.focusable = this.querySelectorAll('sm-button:not([disabled]), button:not([disabled]), [href], sm-input, input, sm-select, select, sm-checkbox, sm-textarea, textarea, [tabindex]:not([tabindex="-1"])') + this.autoFocus = this.querySelector('[autofocus]') + } + + connectedCallback() { + this.popupBodySlot.addEventListener('slotchange', () => { + this.forms = this.querySelectorAll('sm-form'); + this.updateFocusableList() + }); + this.popupContainer.addEventListener('mousedown', e => { + if (e.target === this.popupContainer && !this.pinned) { + if (this.pinned) { + this.setStateOpen(); + } else + this.hide(); + } + }); + + const resizeObserver = new ResizeObserver(entries => { + for (let entry of entries) { + if (entry.contentBoxSize) { + // Firefox implements `contentBoxSize` as a single content rect, rather than an array + const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize; + this.threshold = contentBoxSize.blockSize.height * 0.3; + } else { + this.threshold = entry.contentRect.height * 0.3; + } + } + }); + resizeObserver.observe(this); + + this.mutationObserver = new MutationObserver(entries => { + this.updateFocusableList() + }) + this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true }) + + this.addEventListener('keydown', this.detectFocus); + this.popupHeader.addEventListener('touchstart', this.handleTouchStart, { passive: true }); + } + disconnectedCallback() { + this.removeEventListener('keydown', this.detectFocus); + resizeObserver.unobserve(); + this.mutationObserver.disconnect() + this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true }); + } + attributeChangedCallback(name) { + if (name === 'open') { + if (this.hasAttribute('open')) { + this.show(); + } + } + } +}); \ No newline at end of file diff --git a/js/floExchangeAPI.js b/js/floExchangeAPI.js new file mode 100644 index 0000000..8fac499 --- /dev/null +++ b/js/floExchangeAPI.js @@ -0,0 +1,1110 @@ +'use strict'; + +(function(EXPORTS) { + const exchangeAPI = EXPORTS; + + /*Kademlia DHT K-bucket implementation as a binary tree.*/ + /** + * Implementation of a Kademlia DHT k-bucket used for storing + * contact (peer node) information. + * + * @extends EventEmitter + */ + function BuildKBucket(options = {}) { + /** + * `options`: + * `distance`: Function + * `function (firstId, secondId) { return distance }` An optional + * `distance` function that gets two `id` Uint8Arrays + * and return distance (as number) between them. + * `arbiter`: Function (Default: vectorClock arbiter) + * `function (incumbent, candidate) { return contact; }` An optional + * `arbiter` function that givent two `contact` objects with the same `id` + * returns the desired object to be used for updating the k-bucket. For + * more details, see [arbiter function](#arbiter-function). + * `localNodeId`: Uint8Array An optional Uint8Array representing the local node id. + * If not provided, a local node id will be created via `randomBytes(20)`. + * `metadata`: Object (Default: {}) Optional satellite data to include + * with the k-bucket. `metadata` property is guaranteed not be altered by, + * it is provided as an explicit container for users of k-bucket to store + * implementation-specific data. + * `numberOfNodesPerKBucket`: Integer (Default: 20) The number of nodes + * that a k-bucket can contain before being full or split. + * `numberOfNodesToPing`: Integer (Default: 3) The number of nodes to + * ping when a bucket that should not be split becomes full. KBucket will + * emit a `ping` event that contains `numberOfNodesToPing` nodes that have + * not been contacted the longest. + * + * @param {Object=} options optional + */ + + this.localNodeId = options.localNodeId || window.crypto.getRandomValues(new Uint8Array(20)) + this.numberOfNodesPerKBucket = options.numberOfNodesPerKBucket || 20 + this.numberOfNodesToPing = options.numberOfNodesToPing || 3 + this.distance = options.distance || this.distance + // use an arbiter from options or vectorClock arbiter by default + this.arbiter = options.arbiter || this.arbiter + this.metadata = Object.assign({}, options.metadata) + + this.createNode = function() { + return { + contacts: [], + dontSplit: false, + left: null, + right: null + } + } + + this.ensureInt8 = function(name, val) { + if (!(val instanceof Uint8Array)) { + throw new TypeError(name + ' is not a Uint8Array') + } + } + + /** + * @param {Uint8Array} array1 + * @param {Uint8Array} array2 + * @return {Boolean} + */ + this.arrayEquals = function(array1, array2) { + if (array1 === array2) { + return true + } + if (array1.length !== array2.length) { + return false + } + for (let i = 0, length = array1.length; i < length; ++i) { + if (array1[i] !== array2[i]) { + return false + } + } + return true + } + + this.ensureInt8('option.localNodeId as parameter 1', this.localNodeId) + this.root = this.createNode() + + /** + * Default arbiter function for contacts with the same id. Uses + * contact.vectorClock to select which contact to update the k-bucket with. + * Contact with larger vectorClock field will be selected. If vectorClock is + * the same, candidat will be selected. + * + * @param {Object} incumbent Contact currently stored in the k-bucket. + * @param {Object} candidate Contact being added to the k-bucket. + * @return {Object} Contact to updated the k-bucket with. + */ + this.arbiter = function(incumbent, candidate) { + return incumbent.vectorClock > candidate.vectorClock ? incumbent : candidate + } + + /** + * Default distance function. Finds the XOR + * distance between firstId and secondId. + * + * @param {Uint8Array} firstId Uint8Array containing first id. + * @param {Uint8Array} secondId Uint8Array containing second id. + * @return {Number} Integer The XOR distance between firstId + * and secondId. + */ + this.distance = function(firstId, secondId) { + let distance = 0 + let i = 0 + const min = Math.min(firstId.length, secondId.length) + const max = Math.max(firstId.length, secondId.length) + for (; i < min; ++i) { + distance = distance * 256 + (firstId[i] ^ secondId[i]) + } + for (; i < max; ++i) distance = distance * 256 + 255 + return distance + } + + /** + * Adds a contact to the k-bucket. + * + * @param {Object} contact the contact object to add + */ + this.add = function(contact) { + this.ensureInt8('contact.id', (contact || {}).id) + + let bitIndex = 0 + let node = this.root + + while (node.contacts === null) { + // this is not a leaf node but an inner node with 'low' and 'high' + // branches; we will check the appropriate bit of the identifier and + // delegate to the appropriate node for further processing + node = this._determineNode(node, contact.id, bitIndex++) + } + + // check if the contact already exists + const index = this._indexOf(node, contact.id) + if (index >= 0) { + this._update(node, index, contact) + return this + } + + if (node.contacts.length < this.numberOfNodesPerKBucket) { + node.contacts.push(contact) + return this + } + + // the bucket is full + if (node.dontSplit) { + // we are not allowed to split the bucket + // we need to ping the first this.numberOfNodesToPing + // in order to determine if they are alive + // only if one of the pinged nodes does not respond, can the new contact + // be added (this prevents DoS flodding with new invalid contacts) + return this + } + + this._split(node, bitIndex) + return this.add(contact) + } + + /** + * Get the n closest contacts to the provided node id. "Closest" here means: + * closest according to the XOR metric of the contact node id. + * + * @param {Uint8Array} id Contact node id + * @param {Number=} n Integer (Default: Infinity) The maximum number of + * closest contacts to return + * @return {Array} Array Maximum of n closest contacts to the node id + */ + this.closest = function(id, n = Infinity) { + this.ensureInt8('id', id) + + if ((!Number.isInteger(n) && n !== Infinity) || n <= 0) { + throw new TypeError('n is not positive number') + } + + let contacts = [] + + for (let nodes = [this.root], bitIndex = 0; nodes.length > 0 && contacts.length < n;) { + const node = nodes.pop() + if (node.contacts === null) { + const detNode = this._determineNode(node, id, bitIndex++) + nodes.push(node.left === detNode ? node.right : node.left) + nodes.push(detNode) + } else { + contacts = contacts.concat(node.contacts) + } + } + + return contacts + .map(a => [this.distance(a.id, id), a]) + .sort((a, b) => a[0] - b[0]) + .slice(0, n) + .map(a => a[1]) + } + + /** + * Counts the total number of contacts in the tree. + * + * @return {Number} The number of contacts held in the tree + */ + this.count = function() { + // return this.toArray().length + let count = 0 + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + if (node.contacts === null) nodes.push(node.right, node.left) + else count += node.contacts.length + } + return count + } + + /** + * Determines whether the id at the bitIndex is 0 or 1. + * Return left leaf if `id` at `bitIndex` is 0, right leaf otherwise + * + * @param {Object} node internal object that has 2 leafs: left and right + * @param {Uint8Array} id Id to compare localNodeId with. + * @param {Number} bitIndex Integer (Default: 0) The bit index to which bit + * to check in the id Uint8Array. + * @return {Object} left leaf if id at bitIndex is 0, right leaf otherwise. + */ + this._determineNode = function(node, id, bitIndex) { + // *NOTE* remember that id is a Uint8Array and has granularity of + // bytes (8 bits), whereas the bitIndex is the bit index (not byte) + + // id's that are too short are put in low bucket (1 byte = 8 bits) + // (bitIndex >> 3) finds how many bytes the bitIndex describes + // bitIndex % 8 checks if we have extra bits beyond byte multiples + // if number of bytes is <= no. of bytes described by bitIndex and there + // are extra bits to consider, this means id has less bits than what + // bitIndex describes, id therefore is too short, and will be put in low + // bucket + const bytesDescribedByBitIndex = bitIndex >> 3 + const bitIndexWithinByte = bitIndex % 8 + if ((id.length <= bytesDescribedByBitIndex) && (bitIndexWithinByte !== 0)) { + return node.left + } + + const byteUnderConsideration = id[bytesDescribedByBitIndex] + + // byteUnderConsideration is an integer from 0 to 255 represented by 8 bits + // where 255 is 11111111 and 0 is 00000000 + // in order to find out whether the bit at bitIndexWithinByte is set + // we construct (1 << (7 - bitIndexWithinByte)) which will consist + // of all bits being 0, with only one bit set to 1 + // for example, if bitIndexWithinByte is 3, we will construct 00010000 by + // (1 << (7 - 3)) -> (1 << 4) -> 16 + if (byteUnderConsideration & (1 << (7 - bitIndexWithinByte))) { + return node.right + } + + return node.left + } + + /** + * Get a contact by its exact ID. + * If this is a leaf, loop through the bucket contents and return the correct + * contact if we have it or null if not. If this is an inner node, determine + * which branch of the tree to traverse and repeat. + * + * @param {Uint8Array} id The ID of the contact to fetch. + * @return {Object|Null} The contact if available, otherwise null + */ + this.get = function(id) { + this.ensureInt8('id', id) + + let bitIndex = 0 + + let node = this.root + while (node.contacts === null) { + node = this._determineNode(node, id, bitIndex++) + } + + // index of uses contact id for matching + const index = this._indexOf(node, id) + return index >= 0 ? node.contacts[index] : null + } + + /** + * Returns the index of the contact with provided + * id if it exists, returns -1 otherwise. + * + * @param {Object} node internal object that has 2 leafs: left and right + * @param {Uint8Array} id Contact node id. + * @return {Number} Integer Index of contact with provided id if it + * exists, -1 otherwise. + */ + this._indexOf = function(node, id) { + for (let i = 0; i < node.contacts.length; ++i) { + if (this.arrayEquals(node.contacts[i].id, id)) return i + } + + return -1 + } + + /** + * Removes contact with the provided id. + * + * @param {Uint8Array} id The ID of the contact to remove. + * @return {Object} The k-bucket itself. + */ + this.remove = function(id) { + this.ensureInt8('the id as parameter 1', id) + + let bitIndex = 0 + let node = this.root + + while (node.contacts === null) { + node = this._determineNode(node, id, bitIndex++) + } + + const index = this._indexOf(node, id) + if (index >= 0) { + const contact = node.contacts.splice(index, 1)[0] + } + + return this + } + + /** + * Splits the node, redistributes contacts to the new nodes, and marks the + * node that was split as an inner node of the binary tree of nodes by + * setting this.root.contacts = null + * + * @param {Object} node node for splitting + * @param {Number} bitIndex the bitIndex to which byte to check in the + * Uint8Array for navigating the binary tree + */ + this._split = function(node, bitIndex) { + node.left = this.createNode() + node.right = this.createNode() + + // redistribute existing contacts amongst the two newly created nodes + for (const contact of node.contacts) { + this._determineNode(node, contact.id, bitIndex).contacts.push(contact) + } + + node.contacts = null // mark as inner tree node + + // don't split the "far away" node + // we check where the local node would end up and mark the other one as + // "dontSplit" (i.e. "far away") + const detNode = this._determineNode(node, this.localNodeId, bitIndex) + const otherNode = node.left === detNode ? node.right : node.left + otherNode.dontSplit = true + } + + /** + * Returns all the contacts contained in the tree as an array. + * If this is a leaf, return a copy of the bucket. `slice` is used so that we + * don't accidentally leak an internal reference out that might be + * accidentally misused. If this is not a leaf, return the union of the low + * and high branches (themselves also as arrays). + * + * @return {Array} All of the contacts in the tree, as an array + */ + this.toArray = function() { + let result = [] + for (const nodes = [this.root]; nodes.length > 0;) { + const node = nodes.pop() + if (node.contacts === null) nodes.push(node.right, node.left) + else result = result.concat(node.contacts) + } + return result + } + + /** + * Updates the contact selected by the arbiter. + * If the selection is our old contact and the candidate is some new contact + * then the new contact is abandoned (not added). + * If the selection is our old contact and the candidate is our old contact + * then we are refreshing the contact and it is marked as most recently + * contacted (by being moved to the right/end of the bucket array). + * If the selection is our new contact, the old contact is removed and the new + * contact is marked as most recently contacted. + * + * @param {Object} node internal object that has 2 leafs: left and right + * @param {Number} index the index in the bucket where contact exists + * (index has already been computed in a previous + * calculation) + * @param {Object} contact The contact object to update. + */ + this._update = function(node, index, contact) { + // sanity check + if (!this.arrayEquals(node.contacts[index].id, contact.id)) { + throw new Error('wrong index for _update') + } + + const incumbent = node.contacts[index] + const selection = this.arbiter(incumbent, contact) + // if the selection is our old contact and the candidate is some new + // contact, then there is nothing to do + if (selection === incumbent && incumbent !== contact) return + + node.contacts.splice(index, 1) // remove old contact + node.contacts.push(selection) // add more recent contact version + + } + } + + const K_Bucket = exchangeAPI.K_Bucket = function K_Bucket(masterID, backupList) { + const decodeID = function(floID) { + let k = bitjs.Base58.decode(floID); + k.shift(); + k.splice(-4, 4); + const decodedId = Crypto.util.bytesToHex(k); + const nodeIdBigInt = new BigInteger(decodedId, 16); + const nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned(); + const nodeIdNewInt8Array = new Uint8Array(nodeIdBytes); + return nodeIdNewInt8Array; + }; + const _KB = new BuildKBucket({ + localNodeId: decodeID(masterID) + }); + backupList.forEach(id => _KB.add({ + id: decodeID(id), + floID: id + })); + const orderedList = backupList.map(sn => [_KB.distance(decodeID(masterID), decodeID(sn)), sn]) + .sort((a, b) => a[0] - b[0]) + .map(a => a[1]); + const self = this; + + Object.defineProperty(self, 'order', { + get: () => Array.from(orderedList) + }); + + self.closestNode = function(id, N = 1) { + let decodedId = decodeID(id); + let n = N || orderedList.length; + let cNodes = _KB.closest(decodedId, n) + .map(k => k.floID); + return (N == 1 ? cNodes[0] : cNodes); + }; + + self.isBefore = (source, target) => orderedList.indexOf(target) < orderedList.indexOf(source); + self.isAfter = (source, target) => orderedList.indexOf(target) > orderedList.indexOf(source); + self.isPrev = (source, target) => orderedList.indexOf(target) === orderedList.indexOf(source) - 1; + self.isNext = (source, target) => orderedList.indexOf(target) === orderedList.indexOf(source) + 1; + + self.prevNode = function(id, N = 1) { + let n = N || orderedList.length; + if (!orderedList.includes(id)) + throw Error(`${id} is not in KB list`); + let pNodes = orderedList.slice(0, orderedList.indexOf(id)).slice(-n); + return (N == 1 ? pNodes[0] : pNodes); + }; + + self.nextNode = function(id, N = 1) { + let n = N || orderedList.length; + if (!orderedList.includes(id)) + throw Error(`${id} is not in KB list`); + let nNodes = orderedList.slice(orderedList.indexOf(id) + 1).slice(0, n); + return (N == 1 ? nNodes[0] : nNodes); + }; + + } + + const INVALID_SERVER_MSG = "INCORRECT_SERVER_ERROR"; + var nodeList, nodeURL, nodeKBucket; //Container for (backup) node list + + function fetch_api(api, options) { + return new Promise((resolve, reject) => { + let curPos = fetch_api.curPos || 0; + if (curPos >= nodeList.length) + return resolve('No Nodes online'); + let url = "https://" + nodeURL[nodeList[curPos]]; + (options ? fetch(url + api, options) : fetch(url + api)) + .then(result => resolve(result)).catch(error => { + console.warn(nodeList[curPos], 'is offline'); + //try next node + fetch_api.curPos = curPos + 1; + fetch_api(api, options) + .then(result => resolve(result)) + .catch(error => reject(error)) + }); + }) + } + + function ResponseError(status, data) { + if (data === INVALID_SERVER_MSG) + location.reload(); + else if (this instanceof ResponseError) { + this.data = data; + this.status = status; + } else + return new ResponseError(status, data); + } + + function responseParse(response, json_ = true) { + return new Promise((resolve, reject) => { + if (!response.ok) + response.text() + .then(result => reject(ResponseError(response.status, result))) + .catch(error => reject(error)); + else if (json_) + response.json() + .then(result => resolve(result)) + .catch(error => reject(error)); + else + response.text() + .then(result => resolve(result)) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getAccount = function(floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "get_account", + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/account', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getBuyList = function() { + return new Promise((resolve, reject) => { + fetch_api('/list-buyorders') + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getSellList = function() { + return new Promise((resolve, reject) => { + fetch_api('/list-sellorders') + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getTradeList = function() { + return new Promise((resolve, reject) => { + fetch_api('/list-trades') + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getRates = function(asset = null) { + return new Promise((resolve, reject) => { + fetch_api('/get-rates' + (asset ? "?asset=" + asset : "")) + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + + exchangeAPI.getBalance = function(floID = null, token = null) { + return new Promise((resolve, reject) => { + if (!floID && !token) + return reject("Need atleast one argument") + let queryStr = (floID ? "floID=" + floID : "") + + (floID && token ? "&" : "") + + (token ? "token=" + token : ""); + fetch_api('/get-balance?' + queryStr) + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }) + } + + exchangeAPI.getTx = function(txid) { + return new Promise((resolve, reject) => { + if (!txid) + return reject('txid required'); + fetch_api('/get-transaction?txid=' + txid) + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }) + } + + function signRequest(request, signKey) { + if (typeof request !== "object") + throw Error("Request is not an object"); + let req_str = Object.keys(request).sort().map(r => r + ":" + request[r]).join("|"); + return floCrypto.signData(req_str, signKey); + } + + exchangeAPI.getLoginCode = function() { + return new Promise((resolve, reject) => { + fetch_api('/get-login-code') + .then(result => responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }) + } + + /* + exchangeAPI.signUp = function (privKey, code, hash) { + return new Promise((resolve, reject) => { + if (!code || !hash) + return reject("Login Code missing") + let request = { + pubKey: floCrypto.getPubKeyHex(privKey), + floID: floCrypto.getFloID(privKey), + code: code, + hash: hash, + timestamp: Date.now() + }; + request.sign = signRequest({ + type: "create_account", + random: code, + timestamp: request.timestamp + }, privKey); + console.debug(request); + + fetch_api("/signup", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }); + } + */ + + exchangeAPI.login = function(privKey, proxyKey, code, hash) { + return new Promise((resolve, reject) => { + if (!code || !hash) + return reject("Login Code missing") + let request = { + proxyKey: proxyKey, + floID: floCrypto.getFloID(privKey), + pubKey: floCrypto.getPubKeyHex(privKey), + timestamp: Date.now(), + code: code, + hash: hash + }; + if (!privKey || !request.floID) + return reject("Invalid Private key"); + request.sign = signRequest({ + type: "login", + random: code, + proxyKey: proxyKey, + timestamp: request.timestamp + }, privKey); + console.debug(request); + + fetch_api("/login", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)); + }) + } + + exchangeAPI.logout = function(floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "logout", + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api("/logout", { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.buy = function(asset, quantity, max_price, floID, proxySecret) { + return new Promise((resolve, reject) => { + if (typeof quantity !== "number" || quantity <= 0) + return reject(`Invalid quantity (${quantity})`); + else if (typeof max_price !== "number" || max_price <= 0) + return reject(`Invalid max_price (${max_price})`); + let request = { + floID: floID, + asset: asset, + quantity: quantity, + max_price: max_price, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "buy_order", + asset: asset, + quantity: quantity, + max_price: max_price, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/buy', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + + } + + exchangeAPI.sell = function(asset, quantity, min_price, floID, proxySecret) { + return new Promise((resolve, reject) => { + if (typeof quantity !== "number" || quantity <= 0) + return reject(`Invalid quantity (${quantity})`); + else if (typeof min_price !== "number" || min_price <= 0) + return reject(`Invalid min_price (${min_price})`); + let request = { + floID: floID, + asset: asset, + quantity: quantity, + min_price: min_price, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "sell_order", + quantity: quantity, + asset: asset, + min_price: min_price, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/sell', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + + } + + exchangeAPI.cancelOrder = function(type, id, floID, proxySecret) { + return new Promise((resolve, reject) => { + if (type !== "buy" && type !== "sell") + return reject(`Invalid type (${type}): type should be sell (or) buy`); + let request = { + floID: floID, + orderType: type, + orderID: id, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "cancel_order", + order: type, + id: id, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/cancel', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + //receiver should be object eg {floID1: amount1, floID2: amount2 ...} + exchangeAPI.transferToken = function(receiver, token, floID, proxySecret) { + return new Promise((resolve, reject) => { + if (typeof receiver !== 'object' || receiver === null) + return reject("Invalid receiver: parameter is not an object"); + let invalidIDs = [], + invalidAmt = []; + for (let f in receiver) { + if (!floCrypto.validateAddr(f)) + invalidIDs.push(f); + else if (typeof receiver[f] !== "number" || receiver[f] <= 0) + invalidAmt.push(receiver[f]) + } + if (invalidIDs.length) + return reject(INVALID(`Invalid receiver (${invalidIDs})`)); + else if (invalidAmt.length) + return reject(`Invalid amount (${invalidAmt})`); + let request = { + floID: floID, + token: token, + receiver: receiver, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "transfer_token", + receiver: JSON.stringify(receiver), + token: token, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/transfer-token', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.depositFLO = function(quantity, floID, sinkID, privKey, proxySecret = null) { + return new Promise((resolve, reject) => { + if (typeof quantity !== "number" || quantity <= floGlobals.fee) + return reject(`Invalid quantity (${quantity})`); + floBlockchainAPI.sendTx(floID, sinkID, quantity, privKey, 'Deposit FLO in market').then(txid => { + let request = { + floID: floID, + txid: txid, + timestamp: Date.now() + }; + if (!proxySecret) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(privKey); + request.sign = signRequest({ + type: "deposit_flo", + txid: txid, + timestamp: request.timestamp + }, proxySecret || privKey); + console.debug(request); + + fetch_api('/deposit-flo', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + exchangeAPI.withdrawFLO = function(quantity, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + amount: quantity, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "withdraw_flo", + amount: quantity, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/withdraw-flo', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.depositToken = function(token, quantity, floID, sinkID, privKey, proxySecret = null) { + return new Promise((resolve, reject) => { + if (!floCrypto.verifyPrivKey(privKey, floID)) + return reject("Invalid Private Key"); + floTokenAPI.sendToken(privKey, quantity, sinkID, 'Deposit Rupee in market', token).then(txid => { + let request = { + floID: floID, + txid: txid, + timestamp: Date.now() + }; + if (!proxySecret) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(privKey); + request.sign = signRequest({ + type: "deposit_token", + txid: txid, + timestamp: request.timestamp + }, proxySecret || privKey); + console.debug(request); + + fetch_api('/deposit-token', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + exchangeAPI.withdrawToken = function(token, quantity, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + token: token, + amount: quantity, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "withdraw_token", + token: token, + amount: quantity, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/withdraw-token', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.addUserTag = function(tag_user, tag, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + user: tag_user, + tag: tag, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "add_tag", + user: tag_user, + tag: tag, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/add-tag', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.removeUserTag = function(tag_user, tag, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + user: tag_user, + tag: tag, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "remove_tag", + user: tag_user, + tag: tag, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/remove-tag', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) + } + + exchangeAPI.init = function refreshDataFromBlockchain(adminID = floGlobals.adminID, appName = floGlobals.application) { + return new Promise((resolve, reject) => { + let nodes, lastTx; + try { + nodes = JSON.parse(localStorage.getItem('exchange-nodes')); + if (typeof nodes !== 'object' || nodes === null) + throw Error('nodes must be an object') + else + lastTx = parseInt(localStorage.getItem('exchange-lastTx')) || 0; + } catch (error) { + nodes = {}; + lastTx = 0; + } + floBlockchainAPI.readData(adminID, { + ignoreOld: lastTx, + sentOnly: true, + pattern: appName + }).then(result => { + result.data.reverse().forEach(data => { + var content = JSON.parse(data)[appName]; + //Node List + if (content.Nodes) { + if (content.Nodes.remove) + for (let n of content.Nodes.remove) + delete nodes[n]; + if (content.Nodes.add) + for (let n in content.Nodes.add) + nodes[n] = content.Nodes.add[n]; + } + }); + localStorage.setItem('exchange-lastTx', result.totalTxs); + localStorage.setItem('exchange-nodes', JSON.stringify(nodes)); + nodeURL = nodes; + nodeKBucket = new K_Bucket(adminID, Object.keys(nodeURL)); + nodeList = nodeKBucket.order; + resolve(nodes); + }).catch(error => reject(error)); + }) + } + + exchangeAPI.clearAllLocalData = function() { + localStorage.removeItem('exchange-nodes'); + localStorage.removeItem('exchange-lastTx'); + localStorage.removeItem('exchange-proxy_secret'); + localStorage.removeItem('exchange-user_ID'); + location.reload(); + } + +})('object' === typeof module ? module.exports : window.floExchangeAPI = {}); \ No newline at end of file diff --git a/js/floTokenAPI.js b/js/floTokenAPI.js new file mode 100644 index 0000000..3233279 --- /dev/null +++ b/js/floTokenAPI.js @@ -0,0 +1,53 @@ +'use strict'; + +/* Token Operator to send/receive tokens from blockchain using API calls*/ +(function(GLOBAL) { + const floTokenAPI = GLOBAL.floTokenAPI = { + fetch_api: function(apicall) { + return new Promise((resolve, reject) => { + console.log(floGlobals.tokenURL + apicall); + fetch(floGlobals.tokenURL + apicall).then(response => { + if (response.ok) + response.json().then(data => resolve(data)); + else + reject(response) + }).catch(error => reject(error)) + }) + }, + getBalance: function(floID, token = floGlobals.currency) { + return new Promise((resolve, reject) => { + this.fetch_api(`api/v1.0/getFloAddressBalance?token=${token}&floAddress=${floID}`) + .then(result => resolve(result.balance || 0)) + .catch(error => reject(error)) + }) + }, + getTx: function(txID) { + return new Promise((resolve, reject) => { + this.fetch_api(`api/v1.0/getTransactionDetails/${txID}`).then(res => { + if (res.result === "error") + reject(res.description); + else if (!res.parsedFloData) + reject("Data piece (parsedFloData) missing"); + else if (!res.transactionDetails) + reject("Data piece (transactionDetails) missing"); + else + resolve(res); + }).catch(error => reject(error)) + }) + }, + sendToken: function(privKey, amount, receiverID, message = "", token = floGlobals.currency) { + return new Promise((resolve, reject) => { + let senderID = floCrypto.getFloID(privKey); + if (typeof amount !== "number" || amount <= 0) + return reject("Invalid amount"); + this.getBalance(senderID, token).then(bal => { + if (amount > bal) + return reject("Insufficiant token balance"); + floBlockchainAPI.writeData(senderID, `send ${amount} ${token}# ${message}`, privKey, receiverID) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } + } +})(typeof global !== "undefined" ? global : window); \ No newline at end of file diff --git a/js/main_UI.js b/js/main_UI.js new file mode 100644 index 0000000..a636c9a --- /dev/null +++ b/js/main_UI.js @@ -0,0 +1,370 @@ +// Global variables +const appPages = ['dashboard', 'userinfo', 'subadmin', 'settings']; +const domRefs = {}; +let timerId; +const currentYear = new Date().getFullYear(); + +//Checks for internet connection status +if (!navigator.onLine) + notify( + "There seems to be a problem connecting to the internet, Please check you internet connection.", + "error", + { sound: true } + ); +window.addEventListener("online", () => { + getRef("notification_drawer").clearAll(); + notify("We are back online.", "success"); +}); +window.addEventListener("offline", () => { + notify( + "There seems to be a problem connecting to the internet, Please check you internet connection.", + "error", + { pinned: true, sound: true } + ); +}); + +// Use instead of document.getElementById +function getRef(elementId) { + if (!domRefs.hasOwnProperty(elementId)) { + domRefs[elementId] = { + count: 1, + ref: null, + }; + return document.getElementById(elementId); + } else { + if (domRefs[elementId].count < 3) { + domRefs[elementId].count = domRefs[elementId].count + 1; + return document.getElementById(elementId); + } else { + if (!domRefs[elementId].ref) + domRefs[elementId].ref = document.getElementById(elementId); + return domRefs[elementId].ref; + } + } +} + +// returns dom with specified element +function createElement(tagName, options) { + const { className, textContent, innerHTML, attributes = {} } = options + const elem = document.createElement(tagName) + for (let attribute in attributes) { + elem.setAttribute(attribute, attributes[attribute]) + } + if (className) + elem.className = className + if (textContent) + elem.textContent = textContent + if (innerHTML) + elem.innerHTML = innerHTML + return elem +} + +// Use when a function needs to be executed after user finishes changes +const debounce = (callback, wait) => { + let timeoutId = null; + return (...args) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback.apply(null, args); + }, wait); + }; +} + +// Limits the rate of function execution +function throttle(func, delay) { + // If setTimeout is already scheduled, no need to do anything + if (timerId) { + return; + } + + // Schedule a setTimeout after delay seconds + timerId = setTimeout(function () { + func(); + + // Once setTimeout function execution is finished, timerId = undefined so that in + // the next scroll event function execution can be scheduled by the setTimeout + timerId = undefined; + }, delay); +} + +let zIndex = 10 + // function required for popups or modals to appear + function showPopup(popupId, pinned) { + zIndex++ + getRef(popupId).setAttribute('style', `z-index: ${zIndex}`) + getRef(popupId).show({ pinned }) + return getRef(popupId); + } + + // hides the popup or modal + function hidePopup() { + if (popupStack.peek() === undefined) + return; + popupStack.peek().popup.hide() + } + + +// displays a popup for asking permission. Use this instead of JS confirm +const getConfirmation = (title, message, cancelText = 'Cancel', confirmText = 'OK') => { + return new Promise(resolve => { + showPopup('confirmation_popup', true) + getRef('confirm_title').textContent = title; + getRef('confirm_message').textContent = message; + let cancelButton = getRef('confirmation_popup').children[2].children[0], + submitButton = getRef('confirmation_popup').children[2].children[1] + submitButton.textContent = confirmText + cancelButton.textContent = cancelText + submitButton.onclick = () => { + hidePopup() + resolve(true); + } + cancelButton.onclick = () => { + hidePopup() + resolve(false); + } + }) +} + +// displays a popup for asking user input. Use this instead of JS prompt +async function getPromptInput(title, message = '', isPassword = true, cancelText = 'Cancel', confirmText = 'OK') { + showPopup('prompt_popup', true) + getRef('prompt_title').textContent = title; + let input = getRef('prompt_input'); + input.setAttribute("placeholder", message) + let buttons = getRef('prompt_popup').querySelectorAll("sm-button"); + if (isPassword) + input.setAttribute("type", "text") + else + input.setAttribute("type", "password") + input.focusIn() + buttons[0].textContent = cancelText; + buttons[1].textContent = confirmText; + return new Promise((resolve, reject) => { + buttons[0].onclick = () => { + hidePopup() + return; + } + buttons[1].onclick = () => { + let value = input.value; + hidePopup() + resolve(value) + } + }) +} + +//Function for displaying toast notifications. pass in error for mode param if you want to show an error. +function notify(message, type, options = {}) { + const { pinned = false, sound = false } = options + let icon + switch (type) { + case 'success': + icon = `` + break; + case 'error': + icon = `` + break; + } + getRef("notification_drawer").push(message, { pinned, icon }); + if (navigator.onLine && sound) { + getRef("notification_sound").currentTime = 0; + getRef("notification_sound").play(); + } +} + +function getFormatedTime(time, relative) { + try { + if (String(time).indexOf("_")) time = String(time).split("_")[0]; + const intTime = parseInt(time); + if (String(intTime).length < 13) time *= 1000; + let timeFrag = new Date(intTime).toString().split(" "), + day = timeFrag[0], + month = timeFrag[1], + date = timeFrag[2], + year = timeFrag[3], + minutes = new Date(intTime).getMinutes(), + hours = new Date(intTime).getHours(), + currentTime = new Date().toString().split(" "); + + minutes = minutes < 10 ? `0${minutes}` : minutes; + let finalHours = ``; + if (hours > 12) finalHours = `${hours - 12}:${minutes}`; + else if (hours === 0) finalHours = `12:${minutes}`; + else finalHours = `${hours}:${minutes}`; + + finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`; + if (relative) { + return `${date} ${month} ${year}`; + } else return `${finalHours} ${month} ${date} ${year}`; + } catch (e) { + console.error(e); + return time; + } +} + +window.addEventListener('hashchange', e => showPage(window.location.hash)) +window.addEventListener("load", () => { + document.body.classList.remove('hide') + document.querySelectorAll('sm-input[data-flo-id]').forEach(input => input.customValidation = floCrypto.validateAddr) + document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex) + showPage(window.location.hash) + document.addEventListener('keyup', (e) => { + if (e.code === 'Escape') { + hidePopup() + } + }) + document.addEventListener("pointerdown", (e) => { + if (e.target.closest("button, sm-button:not([disabled]), .interact")) { + createRipple(e, e.target.closest("button, sm-button, .interact")); + } + }); + document.addEventListener('copy', () => { + notify('copied', 'success') + }) +}); + +function createRipple(event, target) { + const circle = document.createElement("span"); + const diameter = Math.max(target.clientWidth, target.clientHeight); + const radius = diameter / 2; + const targetDimensions = target.getBoundingClientRect(); + circle.style.width = circle.style.height = `${diameter}px`; + circle.style.left = `${event.clientX - (targetDimensions.left + radius)}px`; + circle.style.top = `${event.clientY - (targetDimensions.top + radius)}px`; + circle.classList.add("ripple"); + const rippleAnimation = circle.animate( + [ + { + transform: "scale(3)", + opacity: 0, + }, + ], + { + duration: 1000, + fill: "forwards", + easing: "ease-out", + } + ); + target.append(circle); + rippleAnimation.onfinish = () => { + circle.remove(); + }; +} + +function showPage(targetPage, options = {}) { + const { firstLoad, hashChange } = options + let pageId + if (targetPage === '') { + pageId = 'overview_page' + } + else { + pageId = targetPage.includes('#') ? targetPage.split('#')[1] : targetPage + } + if (!appPages.includes(pageId)) return + document.querySelector('.page:not(.hide)').classList.add('hide') + document.querySelector('.nav-list__item--active').classList.remove('nav-list__item--active') + getRef(pageId).classList.remove('hide') + getRef(pageId).animate([ + { + opacity: 0, + transform: 'translateX(-1rem)' + }, + { + opacity: 1, + transform: 'none' + }, + ], + { + duration: 300, + easing: 'ease' + }) + const targetListItem = document.querySelector(`.nav-list__item[href="#${pageId}"]`) + targetListItem.classList.add('nav-list__item--active') + if (firstLoad && window.innerWidth > 640 && targetListItem.getBoundingClientRect().top > getRef('side_nav').getBoundingClientRect().height) { + getRef('side_nav').scrollTo({ + top: (targetListItem.getBoundingClientRect().top - getRef('side_nav').getBoundingClientRect().top + getRef('side_nav').scrollTop), + behavior: 'smooth' + }) + } + if (hashChange && window.innerWidth < 640) { + getRef('side_nav').close() + } +} +// class based lazy loading +class LazyLoader { + constructor(container, elementsToRender, renderFn, options = {}) { + const { batchSize = 10, freshRender } = options + + this.elementsToRender = elementsToRender + this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || [] + this.renderFn = renderFn + this.intersectionObserver + + this.batchSize = batchSize + this.freshRender = freshRender + + this.lazyContainer = document.querySelector(container) + + this.update = this.update.bind(this) + this.render = this.render.bind(this) + this.init = this.init.bind(this) + this.clear = this.clear.bind(this) + } + init() { + this.intersectionObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + observer.disconnect() + this.render({ lazyLoad: true }) + } + }) + }, { + threshold: 0.3 + }) + this.mutationObserver = new MutationObserver(mutationList => { + mutationList.forEach(mutation => { + if (mutation.type === 'childList') { + if (mutation.addedNodes.length) { + this.intersectionObserver.observe(this.lazyContainer.lastElementChild) + } + } + }) + }) + this.mutationObserver.observe(this.lazyContainer, { + childList: true, + }) + this.render() + } + update(elementsToRender) { + this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || [] + this.render() + } + render(options = {}) { + let { lazyLoad = false } = options + const frag = document.createDocumentFragment(); + if (lazyLoad) { + this.updateStartIndex = this.updateEndIndex + this.updateEndIndex = this.arrayOfElements.length > this.updateEndIndex + this.batchSize ? this.updateEndIndex + this.batchSize : this.arrayOfElements.length + } else { + this.intersectionObserver.disconnect() + this.lazyContainer.innerHTML = ``; + this.updateStartIndex = 0 + this.updateEndIndex = this.arrayOfElements.length > this.batchSize ? this.batchSize : this.arrayOfElements.length + } + for (let index = this.updateStartIndex; index < this.updateEndIndex; index++) { + frag.append(this.renderFn(this.arrayOfElements[index])) + } + this.lazyContainer.append(frag) + // Callback to be called if elements are updated or rendered for first time + if (!lazyLoad && this.freshRender) + this.freshRender() + } + clear() { + this.intersectionObserver.disconnect() + this.mutationObserver.disconnect() + this.lazyContainer.innerHTML = ``; + } + reset() { + this.arrayOfElements = (typeof this.elementsToRender === 'function') ? this.elementsToRender() : this.elementsToRender || [] + this.render() + } +} \ No newline at end of file