feature and UI update

-- added BTC as asset
-- added option to send btc
-- added option to view btc transactions
-- implemented different design for homepage
This commit is contained in:
sairaj mote 2022-07-31 01:00:38 +05:30
parent 766128d2eb
commit 0f97744191
9 changed files with 5056 additions and 989 deletions

1
css/bg-art-5.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 240 240"><defs><style>.a{fill:url(#a);}.b{fill:url(#b);}.c{fill:url(#c);}</style><linearGradient id="a" x1="144.49" y1="221.31" x2="241.6" y2="157.88" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="b" x1="90.96" y1="190.69" x2="180.52" y2="132.19" gradientTransform="matrix(-0.36, 0.73, -0.7, -0.35, 326.09, 97.01)" xlink:href="#a"/><linearGradient id="c" x1="32.7" y1="395.41" x2="125.65" y2="334.7" gradientTransform="matrix(0.86, -0.58, 0.56, 0.83, -241, -210.66)" xlink:href="#a"/></defs><circle class="a" cx="192.35" cy="190.05" r="57.99"/><circle class="b" cx="163.17" cy="139.1" r="43.16"/><circle class="c" cx="30.93" cy="46.87" r="57.09"/></svg>

After

Width:  |  Height:  |  Size: 860 B

View File

@ -96,14 +96,12 @@ a.button {
button {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
position: relative;
display: inline-flex;
border: none;
background-color: transparent;
overflow: hidden;
color: inherit;
-webkit-tap-highlight-color: transparent;
align-items: center;
font-size: 0.9rem;
@ -112,6 +110,7 @@ button {
padding: 0.8rem;
border-radius: 0.3rem;
justify-content: center;
color: inherit;
}
button:focus-visible {
outline: var(--accent-color) solid medium;
@ -121,8 +120,12 @@ button:not(:disabled) {
}
.button {
color: var(--accent-color);
background-color: rgba(var(--text-color), 0.06);
}
.button .icon {
fill: var(--accent-color);
}
.button--primary, .button--danger {
color: rgba(var(--background-color), 1) !important;
}
@ -179,7 +182,6 @@ details summary {
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: pointer;
align-items: center;
@ -249,7 +251,6 @@ strip-option {
--border-radius: 0.2rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@ -305,12 +306,11 @@ ol li::before {
text-overflow: ellipsis;
}
.breakable {
.wrap-around {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
-webkit-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto;
}
@ -504,6 +504,10 @@ ol li::before {
background-color: var(--accent-color);
}
.margin-right-0-3 {
margin-right: 0.3rem;
}
.margin-right-0-5 {
margin-right: 0.5rem;
}
@ -531,26 +535,15 @@ ol li::before {
display: flex;
justify-content: space-between;
margin-bottom: 1.5rem;
min-height: 8rem;
min-height: 5rem;
}
.page__header .grid {
margin-top: auto;
}
.page__header h1 {
margin-top: auto;
font-size: 2rem;
}
.page-layout {
display: grid;
gap: 1.5rem 0;
grid-template-columns: 1.5rem minmax(0, 1fr) 1.5rem;
align-content: flex-start;
}
.page-layout > * {
grid-column: 2/3;
}
#confirmation_popup,
#prompt_popup {
flex-direction: column;
@ -622,19 +615,34 @@ ol li::before {
#sign_in,
#sign_up {
justify-items: center;
align-content: center;
}
#sign_in section,
#sign_up section {
margin-top: -8rem;
width: min(24rem, 100%);
justify-content: center;
align-items: center;
}
#sign_in sm-form,
#sign_up sm-form {
margin: 2rem 0;
}
#sign_in {
display: grid;
padding-top: 0;
align-items: center;
align-content: center;
padding-bottom: 0;
}
#sign_in .illustration {
width: 100%;
height: auto;
background-color: #4d77ff;
border-radius: 1rem;
}
#sign_in section {
background-color: rgba(var(--foreground-color), 1);
padding: 2rem;
border-radius: 0.5rem;
margin-top: -2.3rem;
}
#sign_up .h2 {
margin-bottom: 0.5rem;
}
@ -660,27 +668,44 @@ ol li::before {
}
#main_header {
padding: 1.5rem;
display: grid;
gap: 1.5rem;
padding: 1rem 1rem;
align-items: center;
grid-template-columns: minmax(0, 1fr) auto;
min-height: 4rem;
}
.logged-in-user-id {
background-color: rgba(var(--text-color), 0.06);
max-width: -webkit-fit-content;
max-width: -moz-fit-content;
max-width: fit-content;
padding: 0.4rem 0.8rem 0.4rem 0.5rem;
border-radius: 2rem;
font-weight: 500;
}
#main_card {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto 1fr;
position: relative;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
transition: background-color 0.3s;
background-color: rgba(var(--foreground-color), 1);
background-color: rgba(var(--background-color), 1);
}
#main_navbar {
position: fixed;
display: flex;
background: rgba(var(--text-color), 0.03);
}
#main_navbar.hide-away {
position: absolute;
justify-self: center;
margin: 0.5rem;
}
#main_navbar ul {
border-radius: 1rem;
background-color: rgba(var(--foreground-color), 1);
box-shadow: 0 0.8rem 3rem rgba(0, 0, 0, 0.15);
display: flex;
height: 100%;
width: 100%;
@ -700,7 +725,7 @@ ol li::before {
padding: 0.5rem 0.3rem;
color: var(--text-color);
font-size: 0.8rem;
border-radius: 0.3rem;
border-radius: 0.7rem;
font-weight: 500;
}
.nav-item .icon {
@ -750,10 +775,12 @@ ol li::before {
}
.inner-page {
padding: 0 1.5rem;
display: flex;
flex-direction: column;
padding: 0 1rem;
flex: 1;
overflow-y: auto;
align-content: start;
overflow-y: auto;
}
.password-field label {
@ -799,55 +826,46 @@ ol li::before {
}
#wallet_cards_wrapper {
display: flex;
overflow-x: auto;
-ms-scroll-snap-type: x proximity;
scroll-snap-type: x proximity;
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
}
.balance-card {
scroll-snap-align: start;
padding: 1.5rem;
border-radius: 1rem;
width: min(16rem, 90%);
flex-shrink: 0;
color: rgba(0, 0, 0, 0.8);
display: grid;
gap: 1.5rem;
border-radius: 0.5rem;
padding: 1rem;
background-color: rgba(var(--foreground-color), 1);
}
.balance-card:not(:last-of-type) {
margin-right: 0.5rem;
}
.balance-card:nth-of-type(1) {
background: url("bg-art2.svg") no-repeat bottom right, #c2ffd7;
background-size: contain;
}
.balance-card:nth-of-type(1) .icon {
background-color: rgba(102, 255, 156, 0.5);
}
.balance-card:nth-of-type(2) {
background: url("back.svg") no-repeat bottom right, #fcffa8;
background-size: contain;
}
.balance-card:nth-of-type(2) .icon {
background-color: rgba(255, 234, 0, 0.5);
}
.balance-card > .flex {
margin-bottom: 0.3rem;
}
.balance-card > .flex .icon {
height: 2rem;
width: 2rem;
padding: 0.4rem;
border-radius: 0.3rem;
.balance-card .asset-icon {
position: relative;
height: 2.5rem;
width: 2.5rem;
padding: 0.6rem;
border-radius: 1.1rem;
fill: rgba(0, 0, 0, 0.8);
margin-right: 0.5rem;
margin-right: 0.8rem;
}
.balance-card.rupee-card .asset-icon {
background-color: hsl(141deg, 100%, 70%);
}
.balance-card.flo-card .asset-icon {
fill: white;
background-color: #4d77ff;
}
.balance-card.btc-card .asset-icon {
background-color: rgb(255, 173, 8);
}
#rupee_balance span:first-of-type,
#flo_balance span:first-of-type {
font-size: 2rem;
#flo_balance span:first-of-type,
#btc_balance span:first-of-type {
font-size: 1.5rem;
}
#rupee_balance span:last-of-type,
#flo_balance span:last-of-type {
#rupee_balance span:nth-of-type(2),
#flo_balance span:nth-of-type(2),
#btc_balance span:nth-of-type(2) {
font-size: 1rem;
}
@ -865,9 +883,9 @@ ol li::before {
}
.wallet-action .icon:first-of-type,
.integrated-action-button .icon:first-of-type {
height: 2.5rem;
width: 2.5rem;
padding: 0.6rem;
height: 2.3rem;
width: 2.3rem;
padding: 0.5rem;
fill: rgba(var(--foreground-color), 1);
background-color: var(--accent-color);
border-radius: 2rem;
@ -885,14 +903,15 @@ ol li::before {
font-weight: 500;
font-size: 0.8rem;
white-space: initial;
padding: 0.8rem;
}
.wallet-action .icon {
margin-bottom: 0.8rem;
}
.integrated-action-button {
padding: 0.8rem 0;
justify-content: initial;
padding: 0;
justify-content: flex-start;
}
.integrated-action-button .icon:first-of-type {
margin-right: 1rem;
@ -936,29 +955,40 @@ ol li::before {
fill: rgba(0, 0, 0, 0.8);
}
.remove-card-wrapper {
min-height: 2rem;
}
.receiver-card {
display: grid;
gap: 0.5rem;
padding: 0.5rem 0;
border: none;
}
.receiver-card:not(:last-of-type) {
border-bottom: solid thin rgba(var(--text-color), 0.3);
}
#contacts {
position: relative;
padding: 0;
}
#contacts .scrolling-wrapper {
padding: 0 1.5rem;
}
#saved_ids_list {
display: grid;
gap: 0.5rem;
grid-template-columns: minmax(0, 1fr);
padding-bottom: 5rem;
margin-bottom: 1.5rem;
}
.saved-id {
grid-template-columns: auto 1fr;
gap: 0 0.8rem;
border-radius: 0.3rem;
padding: 0.8rem 0;
border-radius: 0.5rem;
padding: 0.5rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: rgba(var(--foreground-color), 1);
}
.saved-id.highlight {
box-shadow: 0 0 0.1rem 0.1rem var(--accent-color) inset;
@ -1002,6 +1032,12 @@ ol li::before {
font-size: 0.8rem;
}
#saved_ids_tip {
background-color: rgba(var(--text-color), 0.03);
border-radius: 2rem;
padding: 0.5rem 0.8rem 0.5rem 0.5rem;
}
.card {
background-color: rgba(var(--foreground-color), 1);
border-radius: 0.5rem;
@ -1084,7 +1120,7 @@ ol li::before {
position: sticky;
top: 0;
padding: 0.2rem 0 0.5rem 0;
background-color: rgba(var(--foreground-color), 1);
background-color: rgba(var(--background-color), 1);
z-index: 1;
transition: background-color 0.3s;
}
@ -1153,7 +1189,7 @@ ol li::before {
#payments_history {
display: grid;
gap: 2rem;
padding-bottom: 4rem;
margin-top: 1.5rem;
}
.transaction {
@ -1205,6 +1241,76 @@ ol li::before {
grid-area: 1/3/3/4;
}
.btc-tx {
grid-template-columns: auto 1fr auto;
gap: 0.5rem 1rem;
align-items: center;
grid-template-areas: "icon time amount" "icon receiver receiver" "icon txid txid";
}
.btc-tx:not(:last-of-type) {
padding-bottom: 1rem;
}
.btc-tx__amount {
white-space: nowrap;
}
.btc-tx.out .icon {
fill: var(--danger-color);
}
.btc-tx.out .btc-tx__amount {
color: var(--danger-color);
}
.btc-tx.out .btc-tx__amount::before {
content: "- ";
}
.btc-tx.in .icon {
fill: var(--green);
}
.btc-tx.in .btc-tx__amount {
color: var(--green);
}
.btc-tx.in .btc-tx__amount::before {
content: "+ ";
}
.btc-tx.unconfirmed-tx {
grid-template-areas: "icon time amount" "icon receiver receiver" "icon txid txid" "icon unconfirmed unconfirmed";
}
.btc-tx.unconfirmed-tx .icon {
fill: var(--yellow);
}
.btc-tx__icon {
grid-area: icon;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
background-color: rgba(var(--text-color), 0.03);
border-radius: 2rem;
}
.btc-tx__receiver {
grid-area: receiver;
font-weight: 500;
}
.btc-tx__time {
grid-area: time;
font-size: 0.9rem;
color: rgba(var(--text-color), 0.8);
}
.btc-tx__amount {
grid-area: amount;
font-size: 1rem;
font-weight: 700;
}
.btc-tx__id {
grid-area: txid;
font-size: 0.9rem;
color: rgba(var(--text-color), 0.8);
}
.btc-tx .unconfirmed-wrapper {
grid-area: unconfirmed;
padding: 0.5rem 0;
}
.fab {
position: absolute;
right: 0;
@ -1221,11 +1327,8 @@ ol li::before {
#add_address_button {
border-radius: 0.5rem;
color: rgba(var(--background-color), 1);
background-color: var(--accent-color);
}
#add_address_button .icon {
fill: rgba(var(--background-color), 1);
background-color: transparent;
border: solid 0.1rem var(--accent-color);
}
.user-action-result__icon {
@ -1291,7 +1394,7 @@ ol li::before {
display: flex;
gap: 1rem;
align-items: center;
padding: 0.8rem 0;
padding: 1rem;
border-radius: 0.3rem;
background-color: rgba(var(--foreground-color), 1);
}
@ -1463,12 +1566,31 @@ ol li::before {
}
@media screen and (max-width: 40rem) {
.inner-page {
padding-bottom: 7rem;
}
#main_navbar {
bottom: 0.5rem;
justify-self: center;
margin: 0 auto;
left: 0;
right: 0;
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
background-color: rgba(var(--foreground-color), 0.9);
-webkit-backdrop-filter: blur(0.5rem);
backdrop-filter: blur(0.5rem);
}
#main_navbar.hide-away {
bottom: 0;
left: 0;
right: 0;
}
.nav-item {
aspect-ratio: 1/1;
width: 4.5rem;
}
.integrated-action-button .icon:last-of-type {
margin-left: auto;
}
@ -1477,58 +1599,32 @@ ol li::before {
sm-popup {
--width: 24rem;
}
.page-layout {
grid-template-columns: 1fr 90vw 1fr;
}
.popup__header {
grid-column: 1/-1;
padding: 1rem 1.5rem 0 1.5rem;
}
body {
align-items: center;
justify-content: center;
}
#main_card {
height: calc(100vh - 2rem);
width: calc(100vw - 2rem);
grid-template-areas: "header" ".";
position: relative;
border-radius: 0.8rem;
overflow: hidden;
box-shadow: 0 0.1rem 0.2rem rgba(0, 0, 0, 0.05), 0 1rem 3rem rgba(0, 0, 0, 0.2);
}
#main_card:not(.nav-hidden) {
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
grid-template-areas: "nav header" "nav .";
}
#main_header {
grid-area: header;
padding: 1rem 4vw;
background-color: rgba(var(--foreground-color), 1);
}
#main_navbar {
grid-area: nav;
border-top: none;
flex-direction: column;
height: calc(100% - 1rem);
margin: 0.5rem;
border-radius: 0.5rem;
background-color: rgba(37, 110, 255, 0.03);
margin: 1rem;
left: 0;
top: 0;
bottom: 0;
justify-content: center;
}
#main_navbar ul {
flex-direction: column;
gap: 0.5rem;
padding: 0.3rem;
align-self: center;
height: auto;
}
#main_navbar ul li:last-of-type {
margin-top: auto;
}
.nav-item {
aspect-ratio: 1/1;
}
@ -1539,24 +1635,24 @@ ol li::before {
border-radius: 0 1rem 1rem 0;
bottom: auto;
}
.card {
padding: 1.5rem;
}
.balance-card {
padding: 1.5rem;
border-radius: 1rem;
}
#contact > * {
padding: 1rem 12vw;
}
#contact > :last-child {
padding: 0.5rem 12vw;
}
#saved_ids_list {
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
}
body[data-theme=dark] #main_navbar {
background-color: rgba(0, 0, 0, 0.2);
}
}
@media screen and (min-width: 56rem) {
#main_card {
width: 56rem;
height: min(90vh, 48rem);
.inner-page {
padding: 1.5rem 18vw;
}
}
@media (any-hover: hover) {
@ -1564,7 +1660,6 @@ ol li::before {
width: 0.5rem;
height: 0.5rem;
}
::-webkit-scrollbar-thumb {
background: rgba(var(--text-color), 0.3);
border-radius: 1rem;
@ -1572,14 +1667,12 @@ ol li::before {
::-webkit-scrollbar-thumb:hover {
background: rgba(var(--text-color), 0.5);
}
.interact:not([disabled]) {
transition: background-color 0.3s;
}
.interact:not([disabled]):hover {
background-color: rgba(var(--text-color), 0.06);
}
button:not([disabled]),
.button:not([disabled]) {
transition: background-color 0.3s, filter 0.3s;

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -94,7 +94,6 @@ button {
border: none;
background-color: transparent;
overflow: hidden;
color: inherit;
-webkit-tap-highlight-color: transparent;
align-items: center;
font-size: 0.9rem;
@ -103,6 +102,7 @@ button {
padding: 0.8rem;
border-radius: 0.3rem;
justify-content: center;
color: inherit;
&:focus-visible {
outline: var(--accent-color) solid medium;
}
@ -111,7 +111,11 @@ button {
}
}
.button {
color: var(--accent-color);
background-color: rgba(var(--text-color), 0.06);
.icon {
fill: var(--accent-color);
}
&--primary,
&--danger {
color: rgba(var(--background-color), 1) !important;
@ -278,7 +282,7 @@ ol {
text-overflow: ellipsis;
}
.breakable {
.wrap-around {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
@ -470,6 +474,9 @@ ol {
background-color: var(--accent-color);
}
}
.margin-right-0-3 {
margin-right: 0.3rem;
}
.margin-right-0-5 {
margin-right: 0.5rem;
}
@ -493,25 +500,15 @@ ol {
display: flex;
justify-content: space-between;
margin-bottom: 1.5rem;
min-height: 8rem;
min-height: 5rem;
.grid {
margin-top: auto;
}
h1 {
margin-top: auto;
font-size: 2rem;
}
}
}
.page-layout {
display: grid;
gap: 1.5rem 0;
grid-template-columns: 1.5rem minmax(0, 1fr) 1.5rem;
align-content: flex-start;
& > * {
grid-column: 2/3;
}
}
#confirmation_popup,
#prompt_popup {
flex-direction: column;
@ -577,16 +574,31 @@ ol {
#sign_in,
#sign_up {
justify-items: center;
align-content: center;
section {
margin-top: -8rem;
width: min(24rem, 100%);
}
justify-content: center;
align-items: center;
sm-form {
margin: 2rem 0;
}
}
#sign_in {
display: grid;
padding-top: 0;
align-items: center;
align-content: center;
padding-bottom: 0;
.illustration {
width: 100%;
height: auto;
background-color: #4d77ff;
border-radius: 1rem;
}
section {
background-color: rgba(var(--foreground-color), 1);
padding: 2rem;
border-radius: 0.5rem;
margin-top: -2.3rem;
}
}
#sign_up {
.h2 {
margin-bottom: 0.5rem;
@ -612,25 +624,39 @@ ol {
}
}
#main_header {
padding: 1.5rem;
display: grid;
gap: 1.5rem;
padding: 1rem 1rem;
align-items: center;
grid-template-columns: minmax(0, 1fr) auto;
min-height: 4rem;
}
.logged-in-user-id {
background-color: rgba(var(--text-color), 0.06);
max-width: fit-content;
padding: 0.4rem 0.8rem 0.4rem 0.5rem;
border-radius: 2rem;
font-weight: 500;
}
#main_card {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto 1fr;
position: relative;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
transition: background-color 0.3s;
background-color: rgba(var(--foreground-color), 1);
background-color: rgba(var(--background-color), 1);
}
#main_navbar {
position: fixed;
display: flex;
background: rgba(var(--text-color), 0.03);
&.hide-away {
position: absolute;
}
justify-self: center;
margin: 0.5rem;
ul {
border-radius: 1rem;
background-color: rgba(var(--foreground-color), 1);
box-shadow: 0 0.8rem 3rem rgba(0 0 0/ 0.15);
display: flex;
height: 100%;
width: 100%;
@ -650,7 +676,7 @@ ol {
padding: 0.5rem 0.3rem;
color: var(--text-color);
font-size: 0.8rem;
border-radius: 0.3rem;
border-radius: 0.7rem;
font-weight: 500;
.icon {
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
@ -699,10 +725,12 @@ ol {
margin: 0.3rem;
}
.inner-page {
padding: 0 1.5rem;
display: flex;
flex-direction: column;
padding: 0 1rem;
flex: 1;
overflow-y: auto;
align-content: start;
overflow-y: auto;
}
.password-field {
@ -745,52 +773,49 @@ ol {
align-content: flex-start;
}
#wallet_cards_wrapper {
display: flex;
overflow-x: auto;
scroll-snap-type: x proximity;
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
}
.balance-card {
scroll-snap-align: start;
padding: 1.5rem;
border-radius: 1rem;
width: min(16rem, 90%);
flex-shrink: 0;
color: rgba(0, 0, 0, 0.8);
&:not(:last-of-type) {
margin-right: 0.5rem;
display: grid;
gap: 1.5rem;
border-radius: 0.5rem;
padding: 1rem;
background-color: rgba(var(--foreground-color), 1);
.asset-icon {
position: relative;
height: 2.5rem;
width: 2.5rem;
padding: 0.6rem;
border-radius: 1.1rem;
fill: rgba(0, 0, 0, 0.8);
margin-right: 0.8rem;
}
&:nth-of-type(1) {
background: url("bg-art2.svg") no-repeat bottom right, hsl(141, 100%, 88%);
background-size: contain;
.icon {
background-color: hsla(141, 100%, 70%, 0.5);
&.rupee-card {
.asset-icon {
background-color: hsla(141, 100%, 70%);
}
}
&:nth-of-type(2) {
background: url("back.svg") no-repeat bottom right, hsl(62, 100%, 83%);
background-size: contain;
.icon {
background-color: hsla(55, 100%, 50%, 0.5);
&.flo-card {
.asset-icon {
fill: white;
background-color: #4d77ff;
}
}
& > .flex {
margin-bottom: 0.3rem;
.icon {
height: 2rem;
width: 2rem;
padding: 0.4rem;
border-radius: 0.3rem;
fill: rgba(0, 0, 0, 0.8);
margin-right: 0.5rem;
&.btc-card {
.asset-icon {
background-color: rgba(255, 173, 8);
}
}
}
#rupee_balance,
#flo_balance {
#flo_balance,
#btc_balance {
span:first-of-type {
font-size: 2rem;
font-size: 1.5rem;
}
span:last-of-type {
span:nth-of-type(2) {
font-size: 1rem;
}
}
@ -805,9 +830,9 @@ ol {
color: inherit;
font-weight: 500;
.icon:first-of-type {
height: 2.5rem;
width: 2.5rem;
padding: 0.6rem;
height: 2.3rem;
width: 2.3rem;
padding: 0.5rem;
fill: rgba(var(--foreground-color), 1);
background-color: var(--accent-color);
border-radius: 2rem;
@ -824,13 +849,14 @@ ol {
font-weight: 500;
font-size: 0.8rem;
white-space: initial;
padding: 0.8rem;
.icon {
margin-bottom: 0.8rem;
}
}
.integrated-action-button {
padding: 0.8rem 0;
justify-content: initial;
padding: 0;
justify-content: flex-start;
.icon:first-of-type {
margin-right: 1rem;
}
@ -870,25 +896,35 @@ ol {
fill: rgba(0, 0, 0, 0.8);
}
}
.remove-card-wrapper {
min-height: 2rem;
}
.receiver-card {
display: grid;
gap: 0.5rem;
padding: 0.5rem 0;
border: none;
&:not(:last-of-type) {
border-bottom: solid thin rgba(var(--text-color), 0.3);
}
}
#contacts {
position: relative;
padding: 0;
.scrolling-wrapper {
padding: 0 1.5rem;
}
}
#saved_ids_list {
display: grid;
gap: 0.5rem;
grid-template-columns: minmax(0, 1fr);
padding-bottom: 5rem;
margin-bottom: 1.5rem;
}
.saved-id {
grid-template-columns: auto 1fr;
gap: 0 0.8rem;
border-radius: 0.3rem;
padding: 0.8rem 0;
border-radius: 0.5rem;
padding: 0.5rem;
user-select: none;
background-color: rgba(var(--foreground-color), 1);
&.highlight {
box-shadow: 0 0 0.1rem 0.1rem var(--accent-color) inset;
}
@ -931,6 +967,11 @@ ol {
font-size: 0.8rem;
}
}
#saved_ids_tip {
background-color: rgba(var(--text-color), 0.03);
border-radius: 2rem;
padding: 0.5rem 0.8rem 0.5rem 0.5rem;
}
.card {
background-color: rgba(var(--foreground-color), 1);
@ -1012,7 +1053,7 @@ ol {
position: sticky;
top: 0;
padding: 0.2rem 0 0.5rem 0;
background-color: rgba(var(--foreground-color), 1);
background-color: rgba(var(--background-color), 1);
z-index: 1;
transition: background-color 0.3s;
}
@ -1078,7 +1119,7 @@ ol {
#payments_history {
display: grid;
gap: 2rem;
padding-bottom: 4rem;
margin-top: 1.5rem;
}
.transaction {
grid-template-columns: auto 1fr auto;
@ -1132,6 +1173,79 @@ ol {
grid-area: 1/3/3/4;
}
}
.btc-tx {
grid-template-columns: auto 1fr auto;
gap: 0.5rem 1rem;
align-items: center;
grid-template-areas: "icon time amount" "icon receiver receiver" "icon txid txid";
&:not(:last-of-type) {
padding-bottom: 1rem;
}
&__amount {
white-space: nowrap;
}
&.out {
.icon {
fill: var(--danger-color);
}
.btc-tx__amount {
color: var(--danger-color);
&::before {
content: "- ";
}
}
}
&.in {
.icon {
fill: var(--green);
}
.btc-tx__amount {
color: var(--green);
&::before {
content: "+ ";
}
}
}
&.unconfirmed-tx {
grid-template-areas: "icon time amount" "icon receiver receiver" "icon txid txid" "icon unconfirmed unconfirmed";
.icon {
fill: var(--yellow);
}
}
&__icon {
grid-area: icon;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
background-color: rgba(var(--text-color), 0.03);
border-radius: 2rem;
}
&__receiver {
grid-area: receiver;
font-weight: 500;
}
&__time {
grid-area: time;
font-size: 0.9rem;
color: rgba(var(--text-color), 0.8);
}
&__amount {
grid-area: amount;
font-size: 1rem;
font-weight: 700;
}
&__id {
grid-area: txid;
font-size: 0.9rem;
color: rgba(var(--text-color), 0.8);
}
.unconfirmed-wrapper {
grid-area: unconfirmed;
padding: 0.5rem 0;
}
}
.fab {
position: absolute;
right: 0;
@ -1146,11 +1260,8 @@ ol {
}
#add_address_button {
border-radius: 0.5rem;
color: rgba(var(--background-color), 1);
background-color: var(--accent-color);
.icon {
fill: rgba(var(--background-color), 1);
}
background-color: transparent;
border: solid 0.1rem var(--accent-color);
}
.user-action-result__icon {
justify-self: center;
@ -1194,7 +1305,7 @@ ol {
display: flex;
gap: 1rem;
align-items: center;
padding: 0.8rem 0;
padding: 1rem;
border-radius: 0.3rem;
background-color: rgba(var(--foreground-color), 1);
&:not(:last-of-type) {
@ -1366,13 +1477,28 @@ ol {
background-color: rgba(var(--text-color), 0.03);
}
@media screen and (max-width: 40rem) {
.inner-page {
padding-bottom: 7rem;
}
#main_navbar {
bottom: 0.5rem;
justify-self: center;
margin: 0 auto;
left: 0;
right: 0;
width: max-content;
background-color: rgba(var(--foreground-color), 0.9);
backdrop-filter: blur(0.5rem);
&.hide-away {
bottom: 0;
left: 0;
right: 0;
}
}
.nav-item {
aspect-ratio: 1/1;
width: 4.5rem;
}
.integrated-action-button {
.icon:last-of-type {
margin-left: auto;
@ -1383,9 +1509,6 @@ ol {
sm-popup {
--width: 24rem;
}
.page-layout {
grid-template-columns: 1fr 90vw 1fr;
}
.popup__header {
grid-column: 1/-1;
padding: 1rem 1.5rem 0 1.5rem;
@ -1394,40 +1517,22 @@ ol {
align-items: center;
justify-content: center;
}
#main_card {
height: calc(100vh - 2rem);
width: calc(100vw - 2rem);
grid-template-areas: "header" ".";
&:not(.nav-hidden) {
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
grid-template-areas: "nav header" "nav .";
}
position: relative;
border-radius: 0.8rem;
overflow: hidden;
box-shadow: 0 0.1rem 0.2rem rgba(0, 0, 0, 0.05),
0 1rem 3rem rgba(0, 0, 0, 0.2);
}
#main_header {
grid-area: header;
padding: 1rem 4vw;
background-color: rgba(var(--foreground-color), 1);
}
#main_navbar {
grid-area: nav;
border-top: none;
flex-direction: column;
height: calc(100% - 1rem);
margin: 0.5rem;
border-radius: 0.5rem;
background-color: rgba(37 110 255/ 0.03);
margin: 1rem;
left: 0;
top: 0;
bottom: 0;
justify-content: center;
ul {
flex-direction: column;
gap: 0.5rem;
padding: 0.3rem;
li:last-of-type {
margin-top: auto;
}
align-self: center;
height: auto;
}
}
.nav-item {
@ -1443,20 +1548,23 @@ ol {
.card {
padding: 1.5rem;
}
#saved_ids_list {
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
.balance-card {
padding: 1.5rem;
border-radius: 1rem;
}
body[data-theme="dark"] {
#main_navbar {
background-color: rgba(0 0 0/ 0.2);
#contact {
& > * {
padding: 1rem 12vw;
}
& > :last-child {
padding: 0.5rem 12vw;
}
}
}
@media screen and (min-width: 56rem) {
#main_card {
width: 56rem;
height: min(90vh, 48rem);
#saved_ids_list {
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
}
.inner-page {
padding: 1.5rem 18vw;
}
}
@media (any-hover: hover) {
@ -1468,7 +1576,6 @@ ol {
::-webkit-scrollbar-thumb {
background: rgba(var(--text-color), 0.3);
border-radius: 1rem;
&:hover {
background: rgba(var(--text-color), 0.5);
}

File diff suppressed because one or more lines are too long

View File

@ -140,26 +140,26 @@ customElements.define('sm-button',
})
const smForm = document.createElement('template');
smForm.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
box-sizing: border-box;
}
:host{
display: flex;
width: 100%;
}
form{
display: grid;
gap: var(--gap, 1.5rem);
width: 100%;
}
</style>
<form part="form" onsubmit="return false">
<slot></slot>
</form>
`;
<style>
*{
padding: 0;
margin: 0;
box-sizing: border-box;
}
:host{
display: grid;
width: 100%;
}
form{
display: inherit;
gap: var(--gap, 1.5rem);
width: 100%;
}
</style>
<form part="form" onsubmit="return false">
<slot></slot>
</form>
`;
customElements.define('sm-form', class extends HTMLElement {
constructor() {
@ -173,7 +173,7 @@ customElements.define('sm-form', class extends HTMLElement {
this.requiredElements
this.submitButton
this.resetButton
this.allRequiredValid = false;
this.invalidFields = false;
this.debounce = this.debounce.bind(this)
this._checkValidity = this._checkValidity.bind(this)
@ -191,18 +191,13 @@ customElements.define('sm-form', class extends HTMLElement {
};
}
_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;
}
this.invalidFields = this.requiredElements.filter(elem => !elem.isValid)
this.submitButton.disabled = this.invalidFields.length;
}
handleKeydown(e) {
if (e.key === 'Enter' && !e.target.tagName.includes('TEXTAREA')) {
if (this.allRequiredValid) {
if (!this.invalidFields.length) {
if (this.submitButton) {
this.submitButton.click()
}
@ -210,9 +205,8 @@ customElements.define('sm-form', class extends HTMLElement {
bubbles: true,
composed: true,
}))
}
else {
this.requiredElements.find(elem => !elem.isValid).vibrate()
} else {
this.requiredElements.forEach(elem => { if (!elem.isValid) elem.vibrate() })
}
}
}
@ -230,220 +224,230 @@ customElements.define('sm-form', class extends HTMLElement {
this._checkValidity()
}
connectedCallback() {
const slot = this.shadowRoot.querySelector('slot')
slot.addEventListener('slotchange', this.elementsChanged)
this.shadowRoot.querySelector('slot').addEventListener('slotchange', this.elementsChanged)
this.addEventListener('input', this.debounce(this._checkValidity, 100));
this.addEventListener('keydown', this.debounce(this.handleKeydown, 100));
const mutationObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
this.elementsChanged()
}
})
})
mutationObserver.observe(this, { childList: true, subtree: true })
}
disconnectedCallback() {
this.removeEventListener('input', this.debounce(this._checkValidity, 100));
this.removeEventListener('keydown', this.debounce(this.handleKeydown, 100));
mutationObserver.disconnect()
}
})
//Input
const smInput = document.createElement('template')
smInput.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration { display: none; }
input[type=number] {
-moz-appearance:textfield;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0;
}
input::-ms-reveal,
input::-ms-clear {
display: none;
}
input:invalid{
outline: none;
-webkit-box-shadow: none;
box-shadow: none;
}
::-moz-focus-inner{
border: none;
}
:host{
display: flex;
--success-color: #00C853;
--danger-color: red;
--width: 100%;
--icon-gap: 0.5rem;
--background: rgba(var(--text-color, (17,17,17)), 0.06);
}
.hide{
display: none !important;
}
button{
display: flex;
border: none;
background: none;
padding: 0;
border-radius: 1rem;
min-width: 0;
cursor: pointer;
}
button:focus{
outline: var(--accent-color, teal) solid medium;
}
.icon {
height: 1.4rem;
width: 1.4rem;
fill: rgba(var(--text-color, (17,17,17)), 0.6);
}
:host(.round) .input{
border-radius: 10rem;
}
.input {
display: flex;
cursor: text;
min-width: 0;
text-align: left;
align-items: center;
position: relative;
gap: var(--icon-gap);
padding: var(--padding, 0.6rem 0.8rem);
border-radius: var(--border-radius,0.3rem);
transition: opacity 0.3s, box-shadow 0.2s;
background: var(--background);
width: 100%;
outline: none;
min-height: 3.5rem;
}
.input.readonly .clear{
opacity: 0 !important;
margin-right: -2rem;
pointer-events: none !important;
}
.readonly{
pointer-events: none;
}
.input:focus-within:not(.readonly){
box-shadow: 0 0 0 0.1rem var(--accent-color,teal) inset !important;
}
.disabled{
pointer-events: none;
opacity: 0.6;
}
.label {
grid-area: 1/1/2/2;
font-size: inherit;
opacity: .7;
font-weight: 400;
transition: -webkit-transform 0.3s;
transition: transform 0.3s;
transition: transform 0.3s, -webkit-transform 0.3s, color .03;
transform-origin: left;
pointer-events: none;
white-space: nowrap;
overflow: hidden;
width: 100%;
user-select: none;
will-change: transform;
}
.outer-container{
position: relative;
width: var(--width);
}
.container{
width: 100%;
display: grid;
grid-template-columns: 1fr auto;
position: relative;
align-items: center;
}
input{
grid-area: 1/1/2/2;
font-size: inherit;
border: none;
background: transparent;
outline: none;
color: inherit;
font-family: inherit;
width: 100%;
caret-color: var(--accent-color, teal);
}
:host([animate]) .input:focus-within .container input,
.animate-placeholder .container input {
-webkit-transform: translateY(0.6rem);
-ms-transform: translateY(0.6rem);
transform: translateY(0.6rem);
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration { display: none; }
input[type=number] {
-moz-appearance:textfield;
}
:host([animate]) .input:focus-within .label,
.animate-placeholder .label {
-webkit-transform: translateY(-0.7em) scale(0.8);
-ms-transform: translateY(-0.7em) scale(0.8);
transform: translateY(-0.7em) scale(0.8);
opacity: 1;
color: var(--accent-color,teal)
}
:host([variant="outlined"]) .input {
box-shadow: 0 0 0 1px var(--border-color, rgba(var(--text-color, (17,17,17)), 0.3)) inset;
background: rgba(var(--background-color, (255,255,255)), 1);
}
.animate-placeholder:focus-within:not(.readonly) .label{
color: var(--accent-color,teal)
}
.feedback-text:not(:empty){
display: flex;
width: 100%;
text-align: left;
font-size: 0.9rem;
align-items: center;
padding: 0.8rem 0;
color: rgba(var(--text-color, (17,17,17)), 0.8);
}
.success{
color: var(--success-color);
}
.error{
color: var(--danger-color);
}
.status-icon{
margin-right: 0.2rem;
}
.status-icon--error{
fill: var(--danger-color);
}
.status-icon--success{
fill: var(--success-color);
}
@media (any-hover: hover){
.icon:hover{
background: rgba(var(--text-color, (17,17,17)), 0.1);
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0;
}
}
</style>
<div class="outer-container">
<label part="input" class="input">
<slot name="icon"></slot>
<div class="container">
<input type="text"/>
<div part="placeholder" class="label"></div>
<button class="clear hide" title="Clear" tabindex="-1">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-11.414L9.172 7.757 7.757 9.172 10.586 12l-2.829 2.828 1.415 1.415L12 13.414l2.828 2.829 1.415-1.415L13.414 12l2.829-2.828-1.415-1.415L12 10.586z"/></svg>
</button>
</div>
<slot name="right"></slot>
</label>
<p class="feedback-text"></p>
</div>
`;
input::-ms-reveal,
input::-ms-clear {
display: none;
}
input:invalid{
outline: none;
-webkit-box-shadow: none;
box-shadow: none;
}
::-moz-focus-inner{
border: none;
}
:host{
display: flex;
--success-color: #00C853;
--danger-color: red;
--width: 100%;
--icon-gap: 0.5rem;
--min-height: 3.2rem;
--background: rgba(var(--text-color, (17,17,17)), 0.06);
}
.hide{
display: none !important;
}
button{
display: flex;
border: none;
background: none;
padding: 0;
border-radius: 1rem;
min-width: 0;
cursor: pointer;
}
button:focus{
outline: var(--accent-color, teal) solid medium;
}
.icon {
height: 1.2rem;
width: 1.2rem;
fill: rgba(var(--text-color, (17,17,17)), 0.6);
}
:host(.round) .input{
border-radius: 10rem;
}
.input {
display: flex;
cursor: text;
min-width: 0;
text-align: left;
align-items: center;
position: relative;
gap: var(--icon-gap);
padding: var(--padding, 0.6rem 0.8rem);
border-radius: var(--border-radius,0.3rem);
transition: opacity 0.3s, box-shadow 0.2s;
background: var(--background);
width: 100%;
outline: none;
min-height: var(--min-height);
}
.input.readonly .clear{
opacity: 0 !important;
margin-right: -2rem;
pointer-events: none !important;
}
.readonly{
pointer-events: none;
}
.input:focus-within:not(.readonly){
box-shadow: 0 0 0 0.1rem var(--accent-color,teal) inset !important;
}
.disabled{
pointer-events: none;
opacity: 0.6;
}
.label {
grid-area: 1/1/2/2;
font-size: inherit;
opacity: .7;
font-weight: 400;
transition: -webkit-transform 0.3s;
transition: transform 0.3s;
transition: transform 0.3s, -webkit-transform 0.3s, color .03;
transform-origin: left;
pointer-events: none;
white-space: nowrap;
overflow: hidden;
width: 100%;
user-select: none;
will-change: transform;
}
.outer-container{
position: relative;
width: var(--width);
}
.container{
width: 100%;
display: grid;
grid-template-columns: 1fr auto;
position: relative;
align-items: center;
}
input{
grid-area: 1/1/2/2;
font-size: inherit;
border: none;
background: transparent;
outline: none;
color: inherit;
font-family: inherit;
width: 100%;
caret-color: var(--accent-color, teal);
}
:host([animate]) .input:focus-within .container input,
.animate-placeholder .container input {
-webkit-transform: translateY(0.6rem);
-ms-transform: translateY(0.6rem);
transform: translateY(0.6rem);
}
:host([animate]) .input:focus-within .label,
.animate-placeholder .label {
-webkit-transform: translateY(-0.7em) scale(0.8);
-ms-transform: translateY(-0.7em) scale(0.8);
transform: translateY(-0.7em) scale(0.8);
opacity: 1;
color: var(--accent-color,teal)
}
:host([variant="outlined"]) .input {
box-shadow: 0 0 0 1px var(--border-color, rgba(var(--text-color, (17,17,17)), 0.3)) inset;
background: rgba(var(--background-color, (255,255,255)), 1);
}
.animate-placeholder:focus-within:not(.readonly) .label{
color: var(--accent-color,teal)
}
.feedback-text:not(:empty){
display: flex;
width: 100%;
text-align: left;
font-size: 0.9rem;
align-items: center;
padding: 0.8rem 0;
color: rgba(var(--text-color, (17,17,17)), 0.8);
}
.success{
color: var(--success-color);
}
.error{
color: var(--danger-color);
}
.status-icon{
margin-right: 0.2rem;
}
.status-icon--error{
fill: var(--danger-color);
}
.status-icon--success{
fill: var(--success-color);
}
@media (any-hover: hover){
.icon:hover{
background: rgba(var(--text-color, (17,17,17)), 0.1);
}
}
</style>
<div class="outer-container">
<label part="input" class="input">
<slot name="icon"></slot>
<div class="container">
<input type="text"/>
<div part="placeholder" class="label"></div>
<button class="clear hide" title="Clear" tabindex="-1">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-11.414L9.172 7.757 7.757 9.172 10.586 12l-2.829 2.828 1.415 1.415L12 13.414l2.828 2.829 1.415-1.415L13.414 12l2.829-2.828-1.415-1.415L12 10.586z"/></svg>
</button>
</div>
<slot name="right"></slot>
</label>
<p class="feedback-text"></p>
</div>
`;
customElements.define('sm-input',
class extends HTMLElement {
@ -471,6 +475,7 @@ customElements.define('sm-input',
this.focusOut = this.focusOut.bind(this);
this.fireEvent = this.fireEvent.bind(this);
this.checkInput = this.checkInput.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.vibrate = this.vibrate.bind(this);
}
@ -552,9 +557,9 @@ customElements.define('sm-input',
this.feedbackText.classList.add('error');
this.feedbackText.classList.remove('success');
this.feedbackText.innerHTML = `
<svg class="status-icon status-icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>
${this._errorText}
`;
<svg class="status-icon status-icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>
${this._errorText}
`;
}
}
return (_isValid && _customValid);
@ -607,6 +612,15 @@ customElements.define('sm-input',
this.feedbackText.textContent = '';
}
}
handleKeydown(e) {
if (e.key.length === 1) {
if (!['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'].includes(e.key)) {
e.preventDefault();
} else if (e.key === '.' && e.target.value.includes('.')) {
e.preventDefault();
}
}
}
vibrate() {
this.outerContainer.animate([
{ transform: 'translateX(-1rem)' },
@ -648,6 +662,10 @@ customElements.define('sm-input',
else if (name === 'type') {
if (this.hasAttribute('type') && this.getAttribute('type') === 'number') {
this.input.setAttribute('inputmode', 'decimal');
this.input.addEventListener('keydown', this.handleKeydown);
} else {
this.input.removeEventListener('keydown', this.handleKeydown);
}
}
else if (name === 'helper-text') {
@ -685,6 +703,7 @@ customElements.define('sm-input',
disconnectedCallback() {
this.input.removeEventListener('input', this.checkInput);
this.clearBtn.removeEventListener('click', this.clear);
this.input.removeEventListener('keydown', this.handleKeydown);
}
})
@ -3081,4 +3100,426 @@ customElements.define('sm-option', class extends HTMLElement {
this.setAttribute('role', 'option')
this.setAttribute('tabindex', '0')
}
})
const smCarousel = document.createElement('template')
smCarousel.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
--arrow-left: 1rem;
--arrow-right: 1rem;
--arrow-top: auto;
--arrow-bottom: auto;
--nav-icon-fill: rgba(var(--background-color, (255,255,255)), 1);
--nav-background-color: rgba(var(--text-color, (17,17,17)), 1);
--nav-box-shadow: 0 0.2rem 0.2rem #00000020, 0 0.5rem 1rem #00000040;
--indicator-top: auto;
--indicator-bottom: -1.5rem;
--indicator-height: 0.4rem;
--indicator-width: 0.4rem;
--indicator-border-radius: 0.4rem;
--indicators-gap: 0.5rem;
--active-indicator-color: var(--accent-color, teal);
}
.carousel__button{
position: absolute;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
cursor: pointer;
min-width: 0;
top: var(--arrow-top);
bottom: var(--arrow-bottom);
border: none;
background: var(--nav-background-color);
-webkit-box-shadow: var(--nav-box-shadow);
box-shadow: var(--nav-box-shadow);
-webkit-tap-highlight-color: transparent;
transition: transform 0.3s, opacity 0.3s;
z-index: 1;
border-radius: 3rem;
padding: 0.5rem;
}
button:focus{
outline: none;
}
button:focus-visible{
outline: rgba(var(--text-color, (17,17,17)), 1) 0.1rem solid;
}
.carousel__button:active{
transform: scale(0.9);
}
.carousel__button--left{
left: var(--arrow-left);
}
.carousel__button--right{
right: var(--arrow-right);
}
.icon {
height: 1.5rem;
width: 1.5rem;
fill: var(--nav-icon-fill);
}
.hide{
display: none !important;
}
:host([indicator]) .carousel-container{
margin-bottom: 2rem;
}
.carousel-container{
position: relative;
display: grid;
width: 100%;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.carousel{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
max-width: 100%;
width: 100%;
overflow: auto hidden;
-ms-scroll-snap-type: x mandatory;
scroll-snap-type: x mandatory;
}
.indicators{
display: -ms-grid;
display: grid;
grid-auto-flow: column;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
position: absolute;
padding: 0.5rem 0;
top: var(--indicator-top);
bottom: var(--indicator-bottom);
gap: var(--indicators-gap);
width: 100%;
}
.indicator{
position: relative;
height: var(--indicator-height);
width: var(--indicator-width);
background: rgba(var(--text-color), 0.3);
border-radius: var(--indicator-border-radius);
-webkit-transition: 0.2s;
-o-transition: 0.2s;
transition: 0.2s;
cursor: pointer;
}
.indicator.active{
background: var(--active-indicator-color);
transform: scale(1.2);
}
slot::slotted(*){
scroll-snap-align: center;
}
:host([align-items="start"]) slot::slotted(*){
scroll-snap-align: start;
}
:host([align-items="center"]) slot::slotted(*){
scroll-snap-align: center;
}
:host([align-items="end"]) slot::slotted(*){
scroll-snap-align: end;
}
@media (hover: hover){
.carousel{
overflow: hidden;
}
.carousel__button{
opacity: 0.8;
}
:host(:hover) .carousel__button{
opacity: 1;
}
.left,.right{
display: none;
}
}
@media (hover: none){
::-webkit-scrollbar-track {
-webkit-box-shadow: none !important;
background-color: transparent !important;
}
::-webkit-scrollbar {
height: 0;
background-color: transparent;
}
.carousel{
overflow: auto none;
}
.carousel__button{
display: none;
}
.left,.right{
display: block;
}
}
</style>
<div class="carousel-container">
<button class="carousel__button carousel__button--left hide">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z"/></svg>
</button>
<div part="carousel" class="carousel">
<slot></slot>
</div>
<button class="carousel__button carousel__button--right hide">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13.172 12l-4.95-4.95 1.414-1.414L16 12l-6.364 6.364-1.414-1.414z"/></svg>
</button>
<div class="indicators"></div>
</div>
`;
customElements.define('sm-carousel', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smCarousel.content.cloneNode(true))
this.isAutoPlaying = false
this.autoPlayInterval = 5000
this.autoPlayTimeout
this.initialTimeout
this.activeSlideNum = 0
this.carouselItems
this.indicators
this.showIndicator = false
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(slideNum) {
this.carousel.scrollTo({
left: (this.carouselItems[slideNum].getBoundingClientRect().left - this.carousel.getBoundingClientRect().left + this.carousel.scrollLeft),
behavior: 'smooth'
})
}
nextSlide() {
if (!this.carouselItems) return
let showSlideNo = (this.activeSlideNum + 1) < this.carouselItems.length ? this.activeSlideNum + 1 : 0
this.showSlide(showSlideNo)
}
autoPlay() {
this.nextSlide()
if (this.isAutoPlaying) {
this.autoPlayTimeout = setTimeout(() => {
this.autoPlay()
}, this.autoPlayInterval);
}
}
startAutoPlay() {
this.setAttribute('autoplay', '')
}
stopAutoPlay() {
this.removeAttribute('autoplay')
}
createIndicator(index) {
let indicator = document.createElement('div')
indicator.classList.add('indicator')
indicator.dataset.rank = index
return indicator
}
handleIndicatorClick(e) {
if (e.target.closest('.indicator')) {
const slideNum = parseInt(e.target.closest('.indicator').dataset.rank)
if (this.activeSlideNum !== slideNum) {
this.showSlide(slideNum)
}
}
}
handleKeyDown(e) {
if (e.code === 'ArrowLeft')
this.scrollRight()
else if (e.code === 'ArrowRight')
this.scrollRight()
}
connectedCallback() {
let frag = document.createDocumentFragment();
this.carouselSlot.addEventListener('slotchange', e => {
this.carouselItems = this.carouselSlot.assignedElements()
this.carouselItems.forEach(item => allElementsObserver.observe(item))
if (this.carouselItems.length > 0) {
firstOptionObserver.observe(this.carouselItems[0])
lastOptionObserver.observe(this.carouselItems[this.carouselItems.length - 1])
}
else {
navButtonLeft.classList.add('hide')
navButtonRight.classList.add('hide')
firstOptionObserver.disconnect()
lastOptionObserver.disconnect()
}
if (this.showIndicator) {
this.indicatorsContainer.innerHTML = ``
this.carouselItems.forEach((item, index) => {
frag.append(
this.createIndicator(index)
)
item.dataset.rank = index
})
this.indicatorsContainer.append(frag)
this.indicators = this.indicatorsContainer.children
}
})
const ioOptions = {
threshold: 0.9,
root: this
}
let activeElements = new Set()
const allElementsObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (this.showIndicator) {
const activeRank = parseInt(entry.target.dataset.rank)
if (entry.isIntersecting) {
this.indicators[activeRank].classList.add('active')
this.activeSlideNum = activeRank
activeElements.add(activeRank)
} else {
this.indicators[activeRank].classList.remove('active')
activeElements.delete(activeRank)
}
}
})
if (activeElements.size === this.carouselItems.length) {
this.indicatorsContainer.classList.add('hide')
} else {
this.indicatorsContainer.classList.remove('hide')
}
}, ioOptions)
const firstOptionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.navButtonLeft.classList.add('hide')
}
else {
this.navButtonLeft.classList.remove('hide')
}
})
},
ioOptions
)
const lastOptionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.navButtonRight.classList.add('hide')
}
else {
this.navButtonRight.classList.remove('hide')
}
})
},
ioOptions
)
const resObs = new ResizeObserver(entries => {
entries.forEach(entry => {
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.scrollDistance = contentBoxSize.inlineSize * 0.6
} else {
this.scrollDistance = entry.contentRect.width * 0.6
}
})
})
resObs.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(name, oldValue, newValue) {
if (oldValue !== newValue) {
if (name === 'indicator') {
this.showIndicator = this.hasAttribute('indicator')
}
if (name === 'autoplay') {
if (this.hasAttribute('autoplay')) {
this.initialTimeout = setTimeout(() => {
this.isAutoPlaying = true
this.autoPlay()
}, this.autoPlayInterval);
}
else {
this.isAutoPlaying = false
clearTimeout(this.autoPlayTimeout)
clearTimeout(this.initialTimeout)
}
}
if (name === 'interval') {
if (this.hasAttribute('interval') && this.getAttribute('interval').trim() !== '') {
this.autoPlayInterval = Math.abs(parseInt(this.getAttribute('interval').trim()))
}
else {
this.autoPlayInterval = 5000
}
}
}
}
disconnectedCallback() {
this.navButtonRight.removeEventListener('click', this.scrollRight)
this.navButtonLeft.removeEventListener('click', this.scrollLeft)
this.indicatorsContainer.removeEventListener('click', this.handleIndicatorClick)
}
})

View File

@ -140,12 +140,11 @@ function transferToExchange() {
}
async function renderSavedUpiIds() {
const frag = document.createDocumentFragment();
let savedUpiIds = []
for (const upiId in floGlobals.savedUserData.upiIds) {
frag.append(render.savedUpiId(upiId));
savedUpiIds.push(render.savedUpiId(upiId))
}
getRef('saved_upi_ids_list').innerHTML = '';
getRef('saved_upi_ids_list').append(frag);
renderElem(getRef('saved_upi_ids_list'), html`${savedUpiIds}`)
}
function saveUpiId() {
const frag = document.createDocumentFragment();
@ -158,10 +157,8 @@ function saveUpiId() {
syncUserData('savedUserData', floGlobals.savedUserData).then(() => {
notify(`Saved ${upiId}`, 'success');
if (pagesData.lastPage === 'settings') {
getRef('saved_upi_ids_list').append(render.savedUpiId(upiId));
getRef('saved_upi_ids_list').append(html.node`${render.savedUpiId(upiId)}`);
} else if (pagesData.lastPage === 'home') {
// getRef('select_topup_upi_id').append(render.savedUpiIdOption(upiId));
// getRef('select_topup_upi_id').parentNode.classList.remove('hide')
getRef('select_withdraw_upi_id').append(render.savedUpiIdOption(upiId));
getRef('select_withdraw_upi_id').parentNode.classList.remove('hide')
}
@ -511,10 +508,10 @@ function getFloIdTitle(floID) {
return floGlobals.savedIds[floID] ? floGlobals.savedIds[floID].title : floID;
}
function formatAmount(amount = 0) {
function formatAmount(amount = 0, currency = 'inr') {
if (!amount)
return '₹0.00';
return amount.toLocaleString(`en-IN`, { style: 'currency', currency: 'INR' })
return amount.toLocaleString(currency === 'inr'? `en-IN`: 'en-US', { style: 'currency', currency })
}
const cashierRejectionErrors = {
@ -525,34 +522,49 @@ const cashierRejectionErrors = {
}
const render = {
savedId(floID, details) {
savedId(floID, details,ref) {
const { title } = details;
const clone = getRef('saved_id_template').content.cloneNode(true).firstElementChild;
clone.dataset.floId = floID;
clone.querySelector('.saved-id__initials').textContent = title.charAt(0);
clone.querySelector('.saved-id__title').textContent = title;
clone.querySelector('.saved-id__flo-id').textContent = floID;
return clone;
return html.for(ref,floID)`
<li class="saved-id grid interact" tabindex="0" data-flo-id="${floID}">
<button class="interact edit-saved icon-only" title="Edit name">
<div class="saved-id__initials">${title.charAt(0)}</div>
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0z" fill="none" /> <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" /> </svg>
</button>
<div class="saved-id__title">${title}</div>
<div class="grid align-center" style="grid-template-columns: 1fr auto;">
<div class="saved-id__flo-id overflow-ellipsis">${floID}</div>
<button class="copy-saved-id icon-only margin-left-0-5" title="Copy FLO ID">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0z" fill="none" /> <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" /> </svg>
</button>
</div>
</li>
`;
},
transactionCard(transactionDetails) {
rupeeTxCard(transactionDetails) {
const { txid, time, sender, receiver, tokenAmount } = transactionDetails;
const clone = getRef('transaction_template').content.cloneNode(true).firstElementChild;
clone.dataset.txid = txid;
clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000);
clone.querySelector('.transaction__amount').textContent = formatAmount(tokenAmount);
let transactionReceiver
let className
let icon
if (sender === myFloID) {
clone.classList.add('sent');
clone.querySelector('.transaction__receiver').textContent = `Sent to ${getFloIdTitle(receiver) || 'Myself'}`;
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon sent" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>`;
className = 'transaction grid sent'
transactionReceiver = `Sent to ${getFloIdTitle(receiver) || 'Myself'}`;
icon = svg`<svg class="icon sent" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>`;
} else if (receiver === myFloID) {
clone.classList.add('received');
clone.querySelector('.transaction__receiver').textContent = `Received from ${getFloIdTitle(sender)}`;
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
className = 'transaction grid received'
transactionReceiver = `Received from ${getFloIdTitle(sender)}`;
icon = svg`<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
} else { //This should not happen unless API returns transaction that does not involve myFloID
row.insertCell().textContent = tx.sender;
row.insertCell().textContent = tx.receiver;
return html`sender: ${sender} | receiver: ${receiver}`;
}
return clone;
return html.node`
<li class="${className}" data-txid="${txid}">
<div class="transaction__icon">${icon}</div>
<div class="transaction__receiver wrap-around">${transactionReceiver}</div>
<time class="transaction__time">${getFormattedTime(time * 1000)}</time>
<div class="transaction__amount">${formatAmount(tokenAmount)}</div>
</li>
`;
},
cashierRequestCard(details) {
const { time, senderID, message: { mode, amount = 0 }, note, tag, vectorClock } = details;
@ -633,23 +645,24 @@ const render = {
return clone;
},
savedUpiId(upiId) {
const clone = getRef('saved_upi_template').content.cloneNode(true).firstElementChild;
clone.dataset.upiId = upiId;
clone.querySelector('.saved-upi__id').textContent = upiId;
return clone;
return html`
<li class="saved-upi" data-upi-id="${upiId}">
<div class="saved-upi__id">${upiId}</div>
<button class="delete-upi" title="Delete this UPI ID">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0z" fill="none" /> <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" /> </svg>
</button>
</li>
`;
},
savedIdPickerCard(floID, { title }) {
return createElement('li', {
className: 'saved-id grid interact',
attributes: { 'tabindex': '0', 'data-flo-id': floID },
innerHTML: `
<div class="saved-id__initials">${title[0]}</div>
<div class="grid gap-0-5">
<h4 class="saved-id__title">${title}</h4>
<div class="saved-id__flo-id overflow-ellipsis">${floID}</div>
</div>
`
})
return html`
<li class="saved-id grid interact" tabindex="0" data-flo-id="${floID}">
<div class="saved-id__initials">${title[0]}</div>
<div class="grid gap-0-5">
<h4 class="saved-id__title">${title}</h4>
<div class="saved-id__flo-id overflow-ellipsis">${floID}</div>
</div>
</li>`
},
savedUpiIdOption(upiId) {
return createElement('sm-option', {
@ -659,10 +672,10 @@ const render = {
}
})
},
paymentsHistory() {
rupeeHistory() {
let paymentTransactions = []
if (paymentsHistoryLoader)
paymentsHistoryLoader.clear()
if (rupeeHistoryLoader)
rupeeHistoryLoader.clear()
getRef('payments_history').innerHTML = '<sm-spinner></sm-spinner>';
floTokenAPI.getAllTxs(myFloID).then(({ transactions }) => {
for (const transactionId in transactions) {
@ -678,23 +691,77 @@ const render = {
}
// solve sorting issue at backend
paymentTransactions.sort((a, b) => b.time - a.time);
if (paymentsHistoryLoader) {
paymentsHistoryLoader.update(paymentTransactions);
if (rupeeHistoryLoader) {
rupeeHistoryLoader.update(paymentTransactions);
} else {
paymentsHistoryLoader = new LazyLoader('#payments_history', paymentTransactions, render.transactionCard);
rupeeHistoryLoader = new LazyLoader('#payments_history', paymentTransactions, render.rupeeTxCard);
}
paymentsHistoryLoader.init();
rupeeHistoryLoader.init();
}).catch(e => {
console.error(e)
})
},
btcTxCard(transactionDetails) {
let { amount, time, txid, sender, receiver, type, block } = transactionDetails;
let transactionReceiver
let icon
// block = null
if (block) {
if (type === 'out') {
transactionReceiver = `Sent to ${receiver}`;
icon = svg`<svg class="icon sent" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>`;
} else if (type === 'in') {
transactionReceiver = `Received from ${sender}`;
icon = svg`<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
} else if (type === 'self') {
transactionReceiver = `Sent to self`;
icon = svg`<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
}
} else {
console.log(transactionDetails)
transactionReceiver = (type === 'out' ? `Sent to ${receiver}` : `Received from ${sender}`);
icon = svg`<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M6,2l0.01,6L10,12l-3.99,4.01L6,22h12v-6l-4-4l4-3.99V2H6z M16,16.5V20H8v-3.5l4-4L16,16.5z"/></g></svg>`;
}
const className = `btc-tx grid ${type} ${block === null ? 'unconfirmed-tx' : ''}`
return html.node`
<li class="${className}" data-txid="${txid}">
<div class="btc-tx__icon">${icon}</div>
<time class="btc-tx__time">${getFormattedTime(time)}</time>
<div class="btc-tx__amount amount-shown" data-btc-amount="${amount}">${formatAmount(amount, 'btc')}</div>
<div class="btc-tx__receiver wrap-around">${transactionReceiver}</div>
<div class="btc-tx__id wrap-around">TXID: ${txid}</div>
</li>
`;
},
btcHistory() {
try {
// render transactions
getRef('payments_history').innerHTML = '<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
getAddressDetails( btc_api.convert.legacy2bech(myFloID)).then(result => {
if (result.txs.length) {
let allTransactions = result.txs;
const filter = getRef('payments_type_filter').querySelector('input:checked').value;
if (filter !== 'all') {
allTransactions = allTransactions.filter(t => filter === 'sent' ? t.type === 'out' : t.type === 'in')
}
console.log(allTransactions)
if (btcHistoryLoader) {
btcHistoryLoader.update(allTransactions)
} else {
btcHistoryLoader = new LazyLoader('#payments_history', allTransactions, render.btcTxCard)
}
btcHistoryLoader.init()
} else {
getRef('payments_history').textContent = 'No transactions found';
}
}).catch(error => console.error(error))
} catch (err) {
notify(err, 'error');
}
},
async savedIds() {
const frag = document.createDocumentFragment();
await organizeSyncedData('savedIds');
getArrayOfSavedIds().forEach(({ floID, details }) => {
frag.append(render.savedId(floID, details));
})
getRef('saved_ids_list').append(frag);
renderElem(getRef('saved_ids_list'), html`${getArrayOfSavedIds().map(({floID, details}) => render.savedId(floID, details, getRef('saved_ids_list')))}`)
},
conditionalSteps() {
if (getRef('topup_wallet__qr_wrapper').open) {
@ -765,6 +832,14 @@ async function refreshBalance(button) {
} catch (e) {
console.error(e)
}
btc_api.getBalance(btc_api.convert.legacy2bech(myFloID)).then(btcBalance => {
if(btcBalance) {
const [beforeDecimal, afterDecimal = '00'] = String(btcBalance).split('.')
renderElem(getRef('btc_balance'), html`<span><b>${beforeDecimal}</b></span>.<span>${afterDecimal}</span>`)
} else {
renderElem(getRef('btc_balance'), html`<span><b>0</b></span>`)
}
})
}
function getArrayOfSavedIds() {
@ -785,7 +860,7 @@ async function saveFloId() {
floGlobals.savedIds[floID] = { title }
buttonLoader('save_flo_id_button', true);
syncUserData('savedIds', floGlobals.savedIds).then(() => {
insertElementAlphabetically(title, render.savedId(floID, { title }))
render.savedIds()
notify(`Saved ${floID}`, 'success');
closePopup();
}).catch(error => {
@ -797,6 +872,7 @@ async function saveFloId() {
delegate(getRef('saved_ids_list'), 'click', '.saved-id', e => {
if (e.target.closest('.edit-saved')) {
const target = e.target.closest('.saved-id');
console.log(target)
getRef('edit_saved_id').setAttribute('value', target.dataset.floId);
getRef('get_new_title').value = getFloIdTitle(target.dataset.floId);
openPopup('edit_saved_popup');
@ -821,15 +897,7 @@ function saveIdChanges() {
title = 'Unknown';
floGlobals.savedIds[floID] = { title }
syncUserData('savedIds', floGlobals.savedIds).then(() => {
const potentialTarget = getRef('saved_ids_list').querySelector(`.saved-id[data-flo-id="${floID}"]`)
if (potentialTarget) {
potentialTarget.querySelector('.saved-id__title').textContent = title;
potentialTarget.querySelector('.saved-id__initials').textContent = title.charAt(0).toUpperCase();
// place the renamed card in alphabetically correct position
const clone = potentialTarget.cloneNode(true);
potentialTarget.remove();
insertElementAlphabetically(title, clone)
}
render.savedIds()
closePopup();
}).catch(error => {
notify(error, 'error');
@ -855,7 +923,7 @@ function deleteSavedId() {
}
const savedIdsObserver = new MutationObserver((mutationList) => {
mutationList.forEach(mutation => {
getRef('saved_ids_tip').textContent = mutation.target.children.length === 0 ? `Click 'Add FLO ID' to add a new FLO ID.` : `Tap on saved IDs to see transaction history.`
conditionalClassToggle(getRef('saved_ids_tip'), 'hide', !mutation.target.children.length);
})
})
@ -883,11 +951,7 @@ getRef('search_saved_ids_picker').addEventListener('input', debounce(async e =>
const fuse = new Fuse(allSavedIds, { keys: ['floID', 'details.title'] })
allSavedIds = fuse.search(searchKey).map(v => v.item)
}
allSavedIds.forEach(({ floID, details }) => {
frag.append(render.savedIdPickerCard(floID, details))
})
getRef('saved_ids_picker_list').innerHTML = '';
getRef('saved_ids_picker_list').append(frag);
renderElem(getRef('saved_ids_picker_list'), html`${allSavedIds.map(({ floID, details }) => render.savedIdPickerCard(floID, details))}`)
if (searchKey !== '') {
const potentialTarget = getRef('saved_ids_picker_list').firstElementChild
if (potentialTarget) {
@ -1028,7 +1092,6 @@ function toggleFilters() {
function applyPaymentsFilters() {
const filter = getRef('payments_type_filter').querySelector('input:checked').value;
getRef('history_applied_filters').innerHTML = ``;
if (filter !== 'all') {
renderElem(getRef('history_applied_filters'),
html`
@ -1042,21 +1105,33 @@ function applyPaymentsFilters() {
</button>`);
}
toggleFilters()
render.paymentsHistory()
if (pagesData.params.asset == 'rupee') {
render.rupeeHistory()
} else {
render.btcHistory()
}
closePopup()
}
function resetPaymentsFilters() {
getRef('payments_type_filter').querySelector('input[value="all"]').checked = true;
render.paymentsHistory()
if (pagesData.params.asset == 'rupee') {
render.rupeeHistory()
} else {
render.btcHistory()
}
closePopup()
toggleFilters()
}
delegate(getRef('history_applied_filters'), 'click', '.applied-filter', e => {
const filter = e.delegateTarget.dataset.filter
const filterValue = e.delegateTarget.dataset.value
// const filter = e.delegateTarget.dataset.filter
// const filterValue = e.delegateTarget.dataset.value
e.delegateTarget.remove()
render.paymentsHistory()
if (pagesData.params.asset == 'rupee') {
render.rupeeHistory()
} else {
render.btcHistory()
}
toggleFilters()
})
@ -1110,4 +1185,166 @@ function signOut() {
}
});
}
function getAddressDetails(address) {
return new Promise((resolve, reject) => {
btc_api.getAddressData(address).then(data => {
console.debug(data);
let details = {};
details.balance = data.balance;
details.address = data.address;
details.txs = data.txs.map(tx => {
let d = {
txid: tx.txid,
time: tx.time,
block: tx.block_no
}
if (tx.outgoing) {
d.type = "out";
d.amount = 0;
d.receiver = new Set();
let change = 0;
tx.outgoing.outputs.forEach(o => {
if (o.address !== address) {
d.receiver.add(o.address)
d.amount += parseFloat(o.value)
} else
change += parseFloat(o.value)
});
d.receiver = Array.from(d.receiver);
d.amount = parseFloat(d.amount.toFixed(8))
d.fee = parseFloat((tx.outgoing.value - (d.amount + change)).toFixed(8))
if (!d.amount && change > 0) {
d.type = "self";
d.amount = change
delete d.receiver;
d.address = address;
}
} else if (tx.incoming) {
d.type = "in";
d.amount = parseFloat(tx.incoming.value);
d.sender = Array.from(new Set(tx.incoming.inputs.map(i => i.address)));
}
return d;
})
resolve(details);
}).catch(error => reject(error))
})
}
function calculateBtcFees() {
fetch('https://bitcoiner.live/api/fees/estimates/latest').then(res => res.json()).then(data => {
const satPerByte = data.estimates['60'].sat_per_vbyte;
const legacyBytes = 200;
const segwitBytes = 77;
const fees = (legacyBytes * satPerByte + (0.25 * satPerByte) * segwitBytes) / Math.pow(10, 8);
getRef('send_fee').value = fees.toFixed(8);
}).catch(e => {
console.error(e)
})
}
function togglePrivateKeyVisibility(input) {
const target = input.closest('sm-input')
target.type = target.type === 'password' ? 'text' : 'password';
target.focusIn()
}
const txParticipantsObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length > 0 && mutation.target.children.length > 1) {
const removeButton = mutation.target.firstElementChild.querySelector('.remove-card')
if (!removeButton) {
const newRemoveButton = html.node`
<button class="remove-card button--small">
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none"></path>
<path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"></path>
</svg>
Remove
</button>
`
mutation.target.firstElementChild.querySelector('.remove-card-wrapper').appendChild(newRemoveButton)
}
} else if (mutation.removedNodes.length > 0 && mutation.target.children.length === 1) {
const removeButton = mutation.target.firstElementChild.querySelector('.remove-card')
if (removeButton) {
removeButton.remove()
}
}
}
})
})
txParticipantsObserver.observe(getRef('receiver_container'), {
childList: true
})
let globalExchangeRate = {}
async function getExchangeRate() {
return new Promise((resolve, reject) => {
Promise.all(['usd', 'inr'].map(cur => fetch(`https://bitpay.com/api/rates/btc/${cur}`))).then(responses => {
Promise.all(responses.map(res => res.json())).then(rates => {
rates.forEach(rate => {
globalExchangeRate[rate.code.toLowerCase()] = rate.rate
})
globalExchangeRate.btc = 1
resolve(globalExchangeRate)
}).catch(err => console.log(err))
}).catch(err => console.log(err))
})
}
getRef('add_receiver').onclick = evt => {
let receiverCard = getRef('receiver_template').content.cloneNode(true)
if (!getRef('receiver_container').children.length) {
receiverCard.querySelector('.remove-card').remove()
}
receiverCard.querySelector('.currency-symbol').innerHTML = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M17.06,11.57C17.65,10.88,18,9.98,18,9c0-1.86-1.27-3.43-3-3.87L15,3h-2v2h-2V3H9v2H6v2h2v10H6v2h3v2h2v-2h2v2h2v-2 c2.21,0,4-1.79,4-4C19,13.55,18.22,12.27,17.06,11.57z M10,7h4c1.1,0,2,0.9,2,2s-0.9,2-2,2h-4V7z M15,17h-5v-4h5c1.1,0,2,0.9,2,2 S16.1,17,15,17z"> </path> </g> </svg>`
getRef('receiver_container').appendChild(receiverCard);
getRef('receiver_container').querySelectorAll('sm-input[data-btc-address]').forEach(input => input.customValidation = btc_api.validateAddress)
}
delegate(getRef('receiver_container'), 'click', '.remove-card', e => {
e.target.closest('.receiver-card').remove()
})
getRef('fees_selector').addEventListener('change', e => {
if (e.target.value !== 'custom') {
getRef('send_fee').readOnly = true;
}
switch (e.target.value) {
case 'custom':
getRef('send_fee').readOnly = false;
getRef('send_fee').focusIn();
getRef('selected_fee_tip').textContent = 'Set custom fee';
break;
case 'suggested':
getRef('selected_fee_tip').textContent = 'Estimated time of confirmation is 1hr'
calculateBtcFees();
break;
}
})
getRef('send_transaction').onclick = evt => {
buttonLoader('send_transaction', true)
const senders = btc_api.convert.legacy2bech(myFloID);
const privKeys = btc_api.convert.wif(myPrivKey);
const receivers = [...getRef('receiver_container').querySelectorAll('.receiver-input')].map(input => input.value.trim());
const amounts = [...getRef('receiver_container').querySelectorAll('.amount-input')].map(input => {
return parseFloat(input.value.trim())
});
const fee = parseFloat(getRef('send_fee').value.trim());
console.debug(senders, receivers, amounts, fee);
btc_api.sendTx(senders, privKeys, receivers, amounts, fee).then(result => {
console.log(result);
closePopup();
getRef('txid').value = result.txid;
openPopup('txid_popup');
getRef('send_tx').reset()
getExchangeRate().then(() => {
calculateBtcFees()
}).catch(e => {
console.error(e)
})
}).catch(error => {
notify(`Error sending transaction \n ${error}`, 'error');
}).finally(_ => {
buttonLoader('send_transaction', false)
})
}

2883
scripts/lib_btc.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
/*jshint esversion: 9 */
// Global variables
const { html, render: renderElem } = uhtml;
const { html, render: renderElem,svg } = uhtml;
const domRefs = {};
let paymentsHistoryLoader = null;
let rupeeHistoryLoader = null;
let btcHistoryLoader = null;
let walletHistoryLoader = null;
let contactHistoryLoader = null;
let paymentRequestsLoader = null;
@ -93,12 +94,10 @@ document.addEventListener('popupopened', async e => {
const frag = document.createDocumentFragment()
switch (e.target.id) {
case 'saved_ids_popup':
getArrayOfSavedIds().forEach(({ floID, details }) => {
frag.append(render.savedIdPickerCard(floID, details))
})
getRef('saved_ids_picker_list').innerHTML = ''
getRef('saved_ids_picker_list').append(frag)
getRef('search_saved_ids_picker').focusIn()
renderElem(getRef('saved_ids_picker_list'), html`${getArrayOfSavedIds().map(({ floID, details }) => render.savedIdPickerCard(floID, details))}`)
setTimeout(() => {
getRef('search_saved_ids_picker').focusIn()
}, 0);
break;
case 'topup_wallet_popup':
case 'withdraw_wallet_popup':
@ -113,6 +112,9 @@ document.addEventListener('popupopened', async e => {
getRef('select_withdraw_upi_id').parentNode.classList.remove('hide')
}
break;
case 'send_btc_popup':
calculateBtcFees();
break;
}
})
document.addEventListener('popupclosed', e => {
@ -208,16 +210,13 @@ function notify(message, mode, options = {}) {
}
}
function getFormattedTime(time, format) {
function getFormattedTime(timestamp, format) {
try {
if (String(time).indexOf('_'))
time = String(time).split('_')[0]
const intTime = parseInt(time)
if (String(intTime).length < 13)
time *= 1000
let [day, month, date, year] = new Date(intTime).toString().split(' '),
minutes = new Date(intTime).getMinutes(),
hours = new Date(intTime).getHours(),
if (String(timestamp).length < 13)
timestamp *= 1000
let [day, month, date, year] = new Date(timestamp).toString().split(' '),
minutes = new Date(timestamp).getMinutes(),
hours = new Date(timestamp).getHours(),
currentTime = new Date().toString().split(' ')
minutes = minutes < 10 ? `0${minutes}` : minutes
@ -233,15 +232,22 @@ function getFormattedTime(time, format) {
switch (format) {
case 'date-only':
return `${month} ${date}, ${year}`;
break;
case 'time-only':
return finalHours;
case 'relative':
// check if timestamp is older than a day
if (Date.now() - new Date(timestamp) < 60 * 60 * 24 * 1000)
return `${finalHours}`;
else
return relativeTime.from(timestamp)
default:
return `${month} ${date}, ${year} at ${finalHours}`;
}
} catch (e) {
console.error(e);
return time;
return timestamp;
}
}
}
// implement event delegation
function delegate(el, event, selector, fn) {
el.addEventListener(event, function (e) {
@ -277,12 +283,18 @@ window.addEventListener("load", () => {
})
});
function toggleFloIDVisibility(hide) {
document.querySelectorAll('.flo-id-wrapper').forEach(elem => {
document.querySelectorAll('.logged-in-user-id').forEach(elem => {
if(hide)
elem.classList.add('hide')
else
elem.classList.remove('hide')
})
document.querySelectorAll('.app-name').forEach(elem => {
if(hide)
elem.classList.remove('hide')
else
elem.classList.add('hide')
})
}
function createRipple(event, target) {
const circle = document.createElement("span");
@ -295,6 +307,10 @@ function createRipple(event, target) {
circle.classList.add("ripple");
const rippleAnimation = circle.animate(
[
{
opacity: 1,
transform: `scale(0)`
},
{
transform: "scale(4)",
opacity: 0,
@ -386,7 +402,13 @@ async function showPage(targetPage, options = {}) {
})
break;
case 'history':
render.paymentsHistory()
if (params.asset) {
if (params.asset === 'rupee') {
render.rupeeHistory()
}else if(params.asset === 'btc'){
render.btcHistory()
}
}
break;
case 'wallet':
const walletTransactions = []
@ -516,8 +538,10 @@ async function showPage(targetPage, options = {}) {
break;
}
if (pageId !== 'history') {
if (paymentsHistoryLoader)
paymentsHistoryLoader.clear()
if (rupeeHistoryLoader)
rupeeHistoryLoader.clear()
if (btcHistoryLoader)
btcHistoryLoader.clear()
}
if (pageId !== 'wallet') {
if (walletHistoryLoader)
@ -668,102 +692,121 @@ const indicatorObserver = new IntersectionObserver(entries => {
threshold: 1
})
// class based lazy loading
class LazyLoader {
constructor(container, elementsToRender, renderFn, options = {}) {
const { batchSize = 10, freshRender, bottomFirst = false } = options
// class based lazy loading
class LazyLoader {
constructor(container, elementsToRender, renderFn, options = {}) {
const { batchSize = 10, freshRender, bottomFirst = false, domUpdated } = options
this.elementsToRender = elementsToRender
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
this.renderFn = renderFn
this.intersectionObserver
this.elementsToRender = elementsToRender
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
this.renderFn = renderFn
this.intersectionObserver
this.batchSize = batchSize
this.freshRender = freshRender
this.bottomFirst = bottomFirst
this.batchSize = batchSize
this.freshRender = freshRender
this.domUpdated = domUpdated
this.bottomFirst = bottomFirst
this.lazyContainer = document.querySelector(container)
this.shouldLazyLoad = false
this.lastScrollTop = 0
this.lastScrollHeight = 0
this.update = this.update.bind(this)
this.render = this.render.bind(this)
this.init = this.init.bind(this)
this.clear = this.clear.bind(this)
}
get elements() {
return this.arrayOfElements
}
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) {
if (this.bottomFirst)
this.intersectionObserver.observe(this.lazyContainer.firstElementChild)
else
this.intersectionObserver.observe(this.lazyContainer.lastElementChild)
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)
}
get elements() {
return this.arrayOfElements
}
init() {
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
observer.disconnect()
this.render({ lazyLoad: true })
}
})
})
this.mutationObserver = new MutationObserver(mutationList => {
mutationList.forEach(mutation => {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length) {
if (this.bottomFirst) {
if (this.lazyContainer.firstElementChild)
this.intersectionObserver.observe(this.lazyContainer.firstElementChild)
} else {
if (this.lazyContainer.lastElementChild)
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 || []
}
render(options = {}) {
let { lazyLoad = false } = options
this.shouldLazyLoad = lazyLoad
const frag = document.createDocumentFragment();
if (lazyLoad) {
if (this.bottomFirst) {
this.updateEndIndex = this.updateStartIndex
this.updateStartIndex = this.updateEndIndex - this.batchSize
} else {
this.updateStartIndex = this.updateEndIndex
this.updateEndIndex = this.updateEndIndex + this.batchSize
}
} else {
this.intersectionObserver.disconnect()
if (this.bottomFirst) {
this.updateEndIndex = this.arrayOfElements.length
this.updateStartIndex = this.updateEndIndex - this.batchSize - 1
} else {
this.updateStartIndex = 0
this.updateEndIndex = this.batchSize
}
this.lazyContainer.innerHTML = ``;
}
})
})
this.mutationObserver.observe(this.lazyContainer, {
childList: true,
})
this.render()
}
update(elementsToRender) {
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
}
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
}
if (this.bottomFirst) {
for (let index = this.updateStartIndex; index < this.updateEndIndex; index++) {
frag.prepend(this.renderFn(this.arrayOfElements[index]))
this.lastScrollHeight = this.lazyContainer.scrollHeight
this.lastScrollTop = this.lazyContainer.scrollTop
this.arrayOfElements.slice(this.updateStartIndex, this.updateEndIndex).forEach((element, index) => {
frag.append(this.renderFn(element))
})
if (this.bottomFirst) {
this.lazyContainer.prepend(frag)
// scroll anchoring for reverse scrolling
this.lastScrollTop += this.lazyContainer.scrollHeight - this.lastScrollHeight
this.lazyContainer.scrollTo({ top: this.lastScrollTop })
this.lastScrollHeight = this.lazyContainer.scrollHeight
} else {
this.lazyContainer.append(frag)
}
if (!lazyLoad && this.bottomFirst) {
this.lazyContainer.scrollTop = this.lazyContainer.scrollHeight
}
// Callback to be called if elements are updated or rendered for first time
if (!lazyLoad && this.freshRender)
this.freshRender()
}
this.lazyContainer.prepend(frag)
} else {
for (let index = this.updateStartIndex; index < this.updateEndIndex; index++) {
frag.append(this.renderFn(this.arrayOfElements[index]))
clear() {
this.intersectionObserver.disconnect()
this.mutationObserver.disconnect()
this.lazyContainer.innerHTML = ``;
}
reset() {
this.arrayOfElements = (typeof this.elementsToRender === 'function') ? this.elementsToRender() : this.elementsToRender || []
this.render()
}
this.lazyContainer.append(frag)
}
if (!lazyLoad && this.bottomFirst)
this.lazyContainer.scrollTo({
top: this.lazyContainer.scrollHeight,
})
// 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()
}
}
function animateTo(element, keyframes, options) {
const anime = element.animate(keyframes, { ...options, fill: 'both' })
anime.finished.then(() => {
@ -780,13 +823,31 @@ function handleMobileChange(e) {
mobileQuery.addEventListener('change', handleMobileChange)
handleMobileChange(mobileQuery)
function showChildElement(id, index) {
[...getRef(id).children].forEach((child, i) => {
if (i === index)
child.classList.remove('hide')
else
child.classList.add('hide')
})
function showChildElement(id, index, options = {}) {
const { mobileView = false, entry, exit } = options
const animOptions = {
duration: 150,
easing: 'ease',
fill: 'forwards'
}
const visibleElement = [...getRef(id).children].find(elem => !elem.classList.contains(mobileView ? 'hide-on-mobile' : 'hide'));
if (visibleElement === getRef(id).children[index]) return;
if (visibleElement) {
if (exit) {
visibleElement.animate(exit, animOptions).onfinish = () => {
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hide')
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hide')
if (entry)
getRef(id).children[index].animate(entry, animOptions)
}
} else {
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hide')
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hide')
}
} else {
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hide')
getRef(id).children[index].animate(entry, animOptions)
}
}
// Reactivity system
@ -880,4 +941,12 @@ function handleVisibilityChange() {
if (floGlobals.loaded && floGlobals.isSubAdmin)
startStatusInterval()
}
}
}
function conditionalClassToggle(el, className, condition) {
if(condition)
el.classList.add(className);
else
el.classList.remove(className);
}