bobsfund/index.html
sairajzero dd961dd928 Adding Bob's fund module
- Added fund closing update
- Added exchangeAPI module
2022-10-30 02:24:16 +05:30

3056 lines
120 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bob's Fund on Blockchain</title>
<link rel="stylesheet" href="css/main.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..900&display=swap" rel="stylesheet">
<script id="floGlobals">
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
const floGlobals = {
blockchain: "FLO",
adminID: "FFXy5pJnfzu2fCDLhpUremyXQjGtFpgCDN",
application: "BobsFund"
}
</script>
<script src="scripts/lib.js"></script>
<script src="scripts/floCrypto.js"></script>
<script src="scripts/floBlockchainAPI.js"></script>
<script src="scripts/floExchangeAPI.js"></script>
<script src="scripts/compactIDB.js"></script>
<script src="scripts/bobs-fund.js"></script>
<script>
function onLoadStartUp() {
floExchangeAPI.init().then(result => {
compactIDB.initDB(floGlobals.application, {
funds: {},
appendix: {}
}).then(result => refresh())
.catch(error => console.error(error))
}).catch(error => reject(error))
}
</script>
</head>
<body onload="onLoadStartUp()" data-theme="light">
<sm-notifications id="notification_drawer"></sm-notifications>
<article id="loading_page" class="page">
<h2 class="h2">Bob's Fund</h2>
<svg id="loader" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><polygon points="17.76 23.78 17.76 40.22 32 48.44 46.24 40.22 46.24 23.78 32 15.56 17.76 23.78"/><polyline points="17.76 23.78 32 32 46.24 23.78"/><line x1="32" y1="48.44" x2="32" y2="32"/><polyline points="32 2 6.02 17 6.02 47"/><polyline points="57.98 47 57.98 17 32 2"/><polyline points="6.02 47 32 62 57.98 47"/></svg>
<h4>Loading data from blockchain</h4>
<footer class="page__footer flex align-center">
<svg id="rm_logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z"/></svg>
<h4 class="h4 color-0-8 weight-500">RanchiMall</h4>
</footer>
</article>
<article id="error_page" class="page hide-completely">
<h2 class="h2">Bob's Fund</h2>
<svg class="sad-face" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<g class="face">
<g class="eyes">
<circle cx="187.52" cy="249.17" r="13.42"/>
<circle cx="312.48" cy="249.17" r="13.42"/>
</g>
<path d="M288,349.35a6.07,6.07,0,0,1-3.81-1.34c-.09-.07-12.31-9.4-34.14-9.4S216,347.94,215.83,348a6.1,6.1,0,0,1-7.59-9.55c.61-.5,15.4-12.08,41.76-12.08s41.15,11.58,41.76,12.08A6.1,6.1,0,0,1,288,349.35Z"/>
<path d="M344.21,237.41a52,52,0,0,1-23.45-6c-15.91-8.09-21.28-19.61-21.5-20.1a4.07,4.07,0,0,1,7.41-3.36c0,.08,4.56,9.49,17.77,16.21S347.93,229,348,229a4.07,4.07,0,0,1,1.63,8A29.26,29.26,0,0,1,344.21,237.41Z"/>
<path d="M155.79,237.41a29.26,29.26,0,0,1-5.45-.43,4.07,4.07,0,0,1,1.63-8c.2,0,10.43,1.87,23.59-4.81s17.73-16.13,17.78-16.23a4.07,4.07,0,0,1,7.4,3.38c-.22.49-5.59,12-21.5,20.1A52,52,0,0,1,155.79,237.41Z"/>
</g>
<path d="M450.16,220.16A200.16,200.16,0,0,0,108.46,78.63,200.69,200.69,0,0,0,57.61,275.68V407.4a10.17,10.17,0,1,0,20.33,0V322.6a201.91,201.91,0,0,0,99.41,84.19v52.27l-11.45.62a10.17,10.17,0,0,0,.54,20.32H167l30.69-1.66V413.45a202.3,202.3,0,0,0,104.62,0v64.88L333,480h.56a10.17,10.17,0,0,0,.54-20.32l-11.45-.62V406.79a201.91,201.91,0,0,0,99.41-84.19v84.8a10.17,10.17,0,1,0,20.33,0V275.68A200.81,200.81,0,0,0,450.16,220.16ZM250,400c-99.16,0-179.83-80.67-179.83-179.83S150.84,40.34,250,40.34,429.83,121,429.83,220.16,349.16,400,250,400Z"/></svg>
<h4 class="margin-bottom-0-5r">Oops, Couldn't get data from blockchain</h4>
<footer class="page__footer flex align-center">
<svg id="rm_logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z"/></svg>
<h4 class="h4 color-0-8 weight-500">RanchiMall</h4>
</footer>
</article>
<header id="main_header" class="hide-completely">
<div class="flex align-center">
<svg id="main_header__logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z"/></svg>
<div class="grid">
<h5 class="header__company-name">RanchiMall</h5>
<h3 class="header__app-name">Bob's Fund</h3>
</div>
</div>
<div id="current_price" class="grid gap-1 flow-column align-center">
<span id="btc-usd-rate" class="weight-700"></span>
<span id="usd-rate" class="weight-700"></span>
</div>
<div class="dropdown">
<button id="profile_button" onclick="changeDropdownState('profile_dropdown', 'toggle', this)">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.93,0a4.8,4.8,0,0,0-4.8,4.8s.13,3.74.54,4.53c.5,1,3,3,4.26,3s3.72-2,4.22-3c.41-.79.58-4.53.58-4.53A4.8,4.8,0,0,0,11.93,0Z"/><path d="M14.47,12.59a4.38,4.38,0,0,1-2.54,1,4.38,4.38,0,0,1-2.53-1c-1,1.78-4,2.69-6.21,3.68-.91.37-1.67,5.3-.61,5.3A18.4,18.4,0,0,0,12.12,24a16.76,16.76,0,0,0,9.17-2.42c1.06,0,.3-4.89-.61-5.3C18.45,15.28,15.42,14.37,14.47,12.59Z"/></svg>
</button>
<ul id="profile_dropdown" class="dropdown__panel hide-completely">
<li class="grid gap-0-5">
<h4 class="weight-700 margin-bottom-0-5r">Preferred currency</h4>
<p>This will convert all amounts to preferred currency.</p>
<sm-select id="currency_selector" align-select="right" class="justify-self-start">
<sm-option value="inr">INR</sm-option>
<sm-option value="usd">USD</sm-option>
</sm-select>
</li>
<li>
<sm-switch id="theme_switcher">
<div slot="left" class="flex weight-700">Dark theme</div>
</sm-switch>
</li>
<li class="interact weight-700" onclick="showPage('admin_page')">
Admin panel
</li>
</ul>
</div>
</header>
<main id="home_page" class="page page-layout hide-completely">
<section id="homepage__hero-section" class="full-bleed page-layout">
<h2 class="h2 weight-700 margin-bottom-1r">Bob's Fund<br>on FLO Blockchain</h2>
<p>
Bob's Fund is a 20 year long term Bitcoin price linked product. Investors are entitled to 100%
of Bitcoin price gains, but they must hold for 20 years. Over a very long time period, investor returns
on an asset like Bitcoin should outstrip returns on conventional assets like real estate and stocks. The
management fees on this product is zero. RanchiMall earns by having invested an equal amount as
every investor, thus the interests of fund manager, and fund investors are totally aligned.
</p>
</section>
<header class="fund-list__header grid margin-bottom-1-5r">
<h3 class="h3">Funds</h3>
<sm-input id="search_investor" type="search" placeholder="Search investor with FLO ID">
<svg slot="icon" 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="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z"/></svg>
</sm-input>
<button id="refresh_button" class="justify-right button--primary">Refresh</button>
</header>
<div class="warning-container grid gap-1">
<p>
When redemption is due, redeem button will appear next to the FLO ID. Enter your private key to redeem.
</p>
<strong style="color: #fb3640">Do not enter private key in similar looking pages.</strong>
</div>
<section id="fund_list"></ul>
<div id="fund_list__empty-state" class="grid hide-completely">
<svg class="icon icon--big" 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-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-4-6h8v2H8v-2zm0-3a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm8 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/></svg>
<h4>No Funds found</h4>
</div>
</main>
<article id="admin_page" class="page page-layout hide-completely">
<header class="flex margin-top-1-5 align-center margin-bottom-1-5r">
<button onclick="showPage('home_page')">
<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="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z"/></svg>
</button>
<h3>Admin Panel</h3>
</header>
<form id="create_fund_form" class="grid gap-1-5">
<sm-switch id="fund_creation_toggle">
<div class="flex" slot="left">
Add investors to existing fund
</div>
</sm-switch>
<section id="fund_details_form" class="grid gap-1-5 margin-bottom-0-5r">
<label class="grid gap-0-5">
Fund start date
<input type="date" name="start_date" required>
</label>
<label class="grid gap-0-5">
Base BTC value ($)
<input type="number" name="btc_base" step="0.01" pattern="[\d,]+(.\d+)?" required>
</label>
<label class="grid gap-0-5">
Base USD rate (₹)
<input type="number" name="usd_rate" min=0 step="0.01" required>
</label>
<div class="grid gap-0-5">
Maximum Duration
<div class="flex">
<input type="number" name="max_pv" inputmode="numeric" required>
<sm-select id="max_pt" align-select="right">
<sm-option value="year(s)" selected>year(s)</sm-option>
<sm-option value="month(s)">month(s)</sm-option>
<sm-option value="week(s)">week(s)</sm-option>
<sm-option value="day(s)">day(s)</sm-option>
</sm-select>
</div>
</div>
<sm-switch id="tapout_toggle">
<div class="flex" slot="left">
Tapout
</div>
</sm-switch>
<section id="tapout_container" class="grid gap-1-5 hide-completely">
<div class="grid gap-0-5">
Tapout window
<div class="flex">
<input type="number" name="tap_wv" inputmode="numeric" disabled required>
<sm-select id="tap_wt" align-select="right">
<sm-option value="year(s)">year(s)</sm-option>
<sm-option value="month(s)" selected>month(s)</sm-option>
<sm-option value="week(s)">week(s)</sm-option>
<sm-option value="day(s)">day(s)</sm-option>
</sm-select>
</div>
</div>
<div class="grid gap-0-5">
Tapout interval(Use comma serated values. e.g. 10, 15...)
<div class="flex">
<input type="text" name="tap_iv" disabled required>
<sm-select id="tap_it" align-select="right">
<sm-option value="year(s)" selected>year(s)</sm-option>
<sm-option value="month(s)">month(s)</sm-option>
</sm-select>
</div>
</div>
</section>
<label class="grid gap-0-5">
Fee
<input type="number" name="fee" min=0 max=100 step="0.01" value="0" inputmode="numeric" required>
</label>
</section>
<div id="fund_selector_container" class="grid margin-bottom-0-5r hide-completely">
<span class="margin-bottom-0-5r">Select Fund</span>
<sm-select id="fund_selector"></sm-select>
</div>
<h4>Add Investors</h4>
<ul id="investors_input_list" class="grid gap-1">
<li class="investor-input grid">
<sm-input placeholder="FLO ID" class="outlined" animate></sm-input>
<sm-input placeholder="Amount(₹)" type="number" min=0 class="outlined" animate></sm-input>
<button class="remove-investor" title="Remove this investor">
<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 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
</button>
</li>
</ul>
<button type="button" class="margin-bottom-1r justify-self-start" style="margin-top: -1rem;" onclick="renderInvestor()">
<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="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></svg>
Add new
</button>
<button id="add_investors_button" type="submit" class="button--primary justify-self-start hide-completely">Add Investors</button>
<button id="create_fund_button" type="submit" class="button--primary justify-self-start">Create Fund</button>
</form>
</article>
<article id="confirm_term_page" class="page page-layout hide-completely">
<header class="flex margin-top-1-5 align-center margin-bottom-1-5r">
<button class="back-button" onclick="showPage('admin_page')">
<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="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z"/></svg>
</button>
<h3>Confirm Details</h3>
</header>
<form class="grid gap-1-5" onsubmit="return false">
<section id="term_details" class="breakable"></section>
<div class="grid">
<h4 class="weight-400 margin-bottom-1r" id="term_admin_id"></h4>
<sm-input id="get_term_private_key" type="password" placeholder="Private key"></sm-input>
</div>
<button id="create_term_button" type="submit" class="button--primary justify-self-start">Create term</button>
</form>
</article>
<article id="confirm_fund_page" class="page page-layout hide-completely">
<header class="flex margin-top-1-5 align-center margin-bottom-1-5r">
<button class="back-button" onclick="showPage('admin_page')">
<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="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z"/></svg>
</button>
<h3>Confirm Details</h3>
</header>
<form class="grid gap-1-5" onsubmit="return false">
<section id="fund_details" class="breakable"></section>
<div class="grid">
<h4 class="weight-400 margin-bottom-1r" id="fund_admin_id"></h4>
<sm-input id="get_fund_private_key" type="password" placeholder="Private key"></sm-input>
</div>
<button id="confirm_fund_button" type="submit" class="button--primary justify-self-start">Create Fund</button>
</form>
</article>
<script id="ui">
const fundInvestorTemplate = document.createElement('template')
fundInvestorTemplate.innerHTML = `
<style>
.flex{
display: flex;
}
.grid{
display: grid;
grid-auto-columns: minmax(0, 1fr);
}
.gap-1{
gap: 1rem;
}
.gap-1-5{
gap: 1.5rem;
}
.flow-column{
grid-auto-flow: column;
}
.justify-end{
justify-content: flex-end;
}
.hide-completely{
display: none !important;
}
.fund-investor{
display: grid;
gap: 1rem;
padding: 1rem 0;
background-color: rgba(var(--foreground-color), 1);
}
.transaction-column{
display: flex;
flex-direction: column;
min-width: 9rem;
}
.space-between{
justify-content: space-between;
}
.label{
font-weight: 500;
font-size: 0.75rem;
margin-bottom: 0.2rem;
color: rgba(var(--text-color), 0.8);
}
.value{
font-weight: 600;
font-size: 0.9rem;
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;
}
@media only screen and (min-width: 640px) {
.justify-self-end{
justify-self: flex-end;
text-align: right;
}
}
@media only screen and (min-width: 1280px) {
.fund-investor{
gap: 1.5rem;
grid-template-columns: 1fr auto;
}
}
</style>
<li class="fund-investor">
<div class="grid">
<span class="label">FLO ID</span>
<span class="value flo-id"></span>
</div>
<div class="grid flow-column gap-1-5 space-between">
<div class="transaction-column">
<span class="label">Invested</span>
<span class="value amount-invested"></span>
</div>
<div class="transaction-column">
<span class="label">Present value</span>
<span class="value net-value" style="color: var(--green)"></span>
</div>
</div>
</li>
`
customElements.define('fund-investor', class extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({
mode: 'open'
})
this.shadow.append(fundInvestorTemplate.content.cloneNode(true))
this.amountInvested
this.netValue
}
set data(obj){
const {
href,
floId,
amountInvested,
netValue,
} = obj
this.amountInvested = amountInvested
this.netValue = netValue
for (let item in this.amountInvested){
this.amountInvested[item] = parseFloat(this.amountInvested[item])
}
for (let item in this.netValue){
this.netValue[item] = parseFloat(this.netValue[item])
}
this.dataset.floId = floId
this.shadow.querySelector('.flo-id').textContent = floId
this.toggleCurrency()
}
toggleCurrency = () => {
this.shadow.querySelector('.amount-invested').textContent = `${this.amountInvested[preferredCurrency].toLocaleString(`en-${preferredCurrency.substring(0,2)}`, {style: 'currency', currency: preferredCurrency})}`
this.shadow.querySelector('.net-value').textContent = `${this.netValue[preferredCurrency].toLocaleString(`en-${preferredCurrency.substring(0,2)}`, {style: 'currency', currency: preferredCurrency})}`
}
connectedCallback(){
document.getElementById('currency_selector').addEventListener('change', this.toggleCurrency)
}
disconnectedCallback(){
document.getElementById('currency_selector').removeEventListener('change', this.toggleCurrency)
}
})
//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: -webkit-box;
display: -ms-flexbox;
display: flex;
--font-size: 1rem;
--border-radius: 0.3rem;
--padding: 0.7rem 1rem;
--background: rgba(var(--text-color), 0.06);
--active-placeholder-color: var(--accent-color);
}
.hide{
opacity: 0 !important;
pointer-events: none !important;
}
.hide-completely{
display: none;
}
.icon {
fill: rgba(var(--text-color), 0.6);
height: 1.4rem;
width: 1.4rem;
border-radius: 1rem;
cursor: pointer;
min-width: 0;
}
:host(.round) .input{
border-radius: 10rem;
}
.input {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
cursor: text;
min-width: 0;
text-align: left;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
gap: 0.5rem;
padding: var(--padding);
border-radius: var(--border-radius);
-webkit-transition: opacity 0.3s;
-o-transition: opacity 0.3s;
transition: opacity 0.3s;
background: var(--background);
width: 100%;
outline: none;
}
.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) inset !important;
}
.disabled{
pointer-events: none;
opacity: 0.6;
}
.label {
opacity: .7;
font-size: var(--font-size);
font-weight: var(--font-weight);
position: absolute;
top: 0;
-webkit-transition: -webkit-transform 0.3s;
transition: -webkit-transform 0.3s;
-o-transition: transform 0.3s;
transition: transform 0.3s;
transition: transform 0.3s, -webkit-transform 0.3s;
-webkit-transform-origin: left;
-ms-transform-origin: left;
transform-origin: left;
pointer-events: none;
white-space: nowrap;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
width: 100%;
user-select: none;
will-change: transform;
}
.outer-container{
position: relative;
width: 100%;
}
.container{
width: 100%;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
position: relative;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
}
input{
font-size: var(--font-size);
border: none;
background: transparent;
outline: none;
color: rgba(var(--text-color), 1);
width: 100%;
font-weight: var(--font-weight);
}
:host(:not(.outlined)) .animate-label .container input {
-webkit-transform: translateY(0.6rem);
-ms-transform: translateY(0.6rem);
transform: translateY(0.6rem);
}
:host(:not(.outlined)) .animate-label .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)
}
:host(.outlined) .input {
box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 0.4) inset;
background: rgba(var(--foreground-color), 1);
}
:host(.outlined) .label {
width: max-content;
margin-left: -0.5rem;
padding: 0 0.5rem;
}
:host(.outlined) .animate-label .label {
-webkit-transform: translate(0.1rem, -1.3rem) scale(0.8);
-ms-transform: translate(0.1rem, -1.3rem) scale(0.8);
transform: translate(0.1rem, -1.3rem) scale(0.8);
opacity: 1;
background: rgba(var(--foreground-color), 1);
}
.animate-label:focus-within:not(.readonly) .label{
color: var(--active-placeholder-color);
}
.feedback-text{
font-size: 0.9rem;
width: 100%;
color: var(--error-color);
padding: 0.6rem 1rem;
text-align: left;
}
.feedback-text:empty{
padding: 0;
}
@media (any-hover: hover){
.icon:hover{
background: rgba(var(--text-color), 0.1);
}
}
</style>
<div class="outer-container">
<label part="input" class="input">
<slot name="icon"></slot>
<div class="container">
<input/>
<div part="placeholder" class="label"></div>
</div>
<svg class="icon clear hide" 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>
</label>
<div class="feedback-text"></div>
</div>
`;
customElements.define('sm-input',
class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smInput.content.cloneNode(true))
}
static get observedAttributes() {
return ['placeholder']
}
get value() {
return this.shadowRoot.querySelector('input').value
}
set value(val) {
this.shadowRoot.querySelector('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')
}
get isValid() {
return this.shadowRoot.querySelector('input').checkValidity()
}
get validity() {
return this.shadowRoot.querySelector('input').validity
}
set disabled(value) {
if (value)
this.shadowRoot.querySelector('.input').classList.add('disabled')
else
this.shadowRoot.querySelector('.input').classList.remove('disabled')
}
set readOnly(value) {
if (value) {
this.shadowRoot.querySelector('input').setAttribute('readonly', '')
this.shadowRoot.querySelector('.input').classList.add('readonly')
} else {
this.shadowRoot.querySelector('input').removeAttribute('readonly')
this.shadowRoot.querySelector('.input').classList.remove('readonly')
}
}
setValidity = (message) => {
this.feedbackText.textContent = message
}
showValidity = () => {
this.feedbackText.classList.remove('hide-completely')
}
hideValidity = () => {
this.feedbackText.classList.add('hide-completely')
}
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.readonly) {
if (this.input.value !== '') {
this.clearBtn.classList.remove('hide')
} else {
this.clearBtn.classList.add('hide')
}
}
if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder') === '') 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')
}
}
connectedCallback() {
this.inputParent = this.shadowRoot.querySelector('.input')
this.clearBtn = this.shadowRoot.querySelector('.clear')
this.label = this.shadowRoot.querySelector('.label')
this.feedbackText = this.shadowRoot.querySelector('.feedback-text')
this.valueChanged = false;
this.readonly = false
this.isNumeric = false
this.min
this.max
this.animate = this.hasAttribute('animate')
this.input = this.shadowRoot.querySelector('input')
this.shadowRoot.querySelector('.label').textContent = this.getAttribute('placeholder')
if (this.hasAttribute('value')) {
this.input.value = this.getAttribute('value')
this.checkInput()
}
if (this.hasAttribute('required')) {
this.input.setAttribute('required', '')
}
if (this.hasAttribute('min')) {
let minValue = this.getAttribute('min')
this.input.setAttribute('min', minValue)
this.min = parseInt(minValue)
}
if (this.hasAttribute('max')) {
let maxValue = this.getAttribute('max')
this.input.setAttribute('max', maxValue)
this.max = parseInt(maxValue)
}
if (this.hasAttribute('minlength')) {
const minValue = this.getAttribute('minlength')
this.input.setAttribute('minlength', minValue)
}
if (this.hasAttribute('maxlength')) {
const maxValue = this.getAttribute('maxlength')
this.input.setAttribute('maxlength', maxValue)
}
if (this.hasAttribute('step')) {
const steps = this.getAttribute('step')
this.input.setAttribute('step', steps)
}
if (this.hasAttribute('pattern')) {
this.input.setAttribute('pattern', this.getAttribute('pattern'))
}
if (this.hasAttribute('readonly')) {
this.input.setAttribute('readonly', '')
this.readonly = true
}
if (this.hasAttribute('disabled')) {
this.inputParent.classList.add('disabled')
}
if (this.hasAttribute('error-text')) {
this.feedbackText.textContent = this.getAttribute('error-text')
}
if (this.hasAttribute('type')) {
if (this.getAttribute('type') === 'number') {
this.input.setAttribute('inputmode', 'numeric')
this.input.setAttribute('type', 'number')
this.isNumeric = true
} else
this.input.setAttribute('type', this.getAttribute('type'))
} else
this.input.setAttribute('type', 'text')
this.input.addEventListener('input', e => {
this.checkInput(e)
})
this.clearBtn.addEventListener('click', e => {
this.value = ''
})
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
if (name === 'placeholder') {
this.shadowRoot.querySelector('.label').textContent = newValue;
this.setAttribute('aria-label', newValue);
}
}
}
})
const smSelect = document.createElement('template')
smSelect.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
--max-height: auto;
--border: solid 1px rgba(var(--text-color), 0.2);
--arrow-fill: rgba(var(--text-color), 0.7);
--selection-font-size: inherit;
}
.icon {
height: 1.4rem;
width: 1.4rem;
margin-left: 0.5rem;
fill: var(--arrow-fill);
}
.hide{
opacity: 0;
pointer-events: none;
}
.select{
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
cursor: pointer;
width: 100%;
-webkit-tap-highlight-color: transparent;
}
.option-text{
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700;
font-size: var(--selection-font-size);
}
.selection{
border-radius: 0.3rem;
display: -ms-grid;
display: grid;
-ms-grid-columns: 1fr auto;
grid-template-columns: 1fr auto;
grid-template-areas: 'heading heading' '. .';
padding: 0.4rem 1rem;
background: rgba(var(--text-color), 0.06);
border: var(--border);
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
outline: none;
}
.selection:focus-visible{
-webkit-box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1);
box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1);
}
:host([align-select="left"]) .options{
left: 0;
}
:host([align-select="right"]) .options{
right: 0;
}
.options{
top: 100%;
margin-top: 0.5rem;
overflow: hidden auto;
position: absolute;
grid-area: options;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
min-width: 100%;
max-height: var(--max-height);
background: rgba(var(--foreground-color), 1);
-webkit-transition: opacity 0.3s, top 0.3s;
-o-transition: opacity 0.3s, top 0.3s;
transition: opacity 0.3s, top 0.3s;
border: solid 1px rgba(var(--text-color), 0.2);
border-radius: 0.3rem;
z-index: 2;
-webkit-box-shadow: 0.4rem 0.8rem 1.2rem #00000030;
box-shadow: 0.4rem 0.8rem 1.2rem #00000030;
}
.rotate{
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg)
}
</style>
<div class="select" >
<div class="selection" tabindex="0">
<div class="option-text"></div>
<svg class="icon toggle" 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 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z"/></svg>
</div>
<div part="options" aria-hidden="true" class="options hide">
<slot></slot>
</div>
</div>`;
customElements.define('sm-select', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smSelect.content.cloneNode(true))
this.availableOptions
this.optionList = this.shadowRoot.querySelector('.options')
this.chevron = this.shadowRoot.querySelector('.toggle')
this.optionText = this.shadowRoot.querySelector('.option-text')
}
static get observedAttributes() {
return ['value']
}
get value() {
return this.getAttribute('value')
}
set value(val) {
this.setAttribute('value', val)
this.optionText.textContent = val;
}
open = () => {
this.optionList.classList.remove('hide')
this.optionList.animate(this.slideDown, this.animationOptions)
this.availableOptions.forEach((element, index) => {
element.setAttribute('tabindex', "0");
})
this.chevron.classList.add('rotate')
this.isOpen = true
}
collapse = () => {
this.optionList.animate(this.slideUp, this.animationOptions)
this.optionList.setAttribute('aria-hidden', 'true')
this.availableOptions.forEach((element, index) => {
element.setAttribute('tabindex', "-1");
})
this.optionList.classList.add('hide')
this.chevron.classList.remove('rotate')
this.isOpen = false
}
connectedCallback() {
let slot = this.shadowRoot.querySelector('.options slot'),
selection = this.shadowRoot.querySelector('.selection'),
previousOption
this.isOpen = false;
this.slideDown = [{
transform: `translateY(-0.5rem)`
},
{
transform: `translateY(0)`
}
],
this.slideUp = [{
transform: `translateY(0)`
},
{
transform: `translateY(-0.5rem)`
}
],
this.animationOptions = {
duration: 300,
fill: "forwards",
easing: 'ease'
}
selection.addEventListener('click', e => {
if (!this.isOpen) {
this.open()
} else {
this.collapse()
}
})
selection.addEventListener('keydown', e => {
if (e.code === 'ArrowDown' || e.code === 'ArrowRight') {
e.preventDefault()
this.availableOptions[0].focus()
}
if (e.code === 'Enter' || e.code === 'Space')
if (!this.isOpen) {
this.open()
} else {
this.collapse()
}
})
this.optionList.addEventListener('keydown', e => {
if (e.code === 'ArrowUp' || e.code === 'ArrowRight') {
e.preventDefault()
if (document.activeElement.previousElementSibling) {
document.activeElement.previousElementSibling.focus()
}
}
if (e.code === 'ArrowDown' || e.code === 'ArrowLeft') {
e.preventDefault()
if (document.activeElement.nextElementSibling)
document.activeElement.nextElementSibling.focus()
}
})
this.addEventListener('optionSelected', e => {
if (previousOption !== e.target) {
this.setAttribute('value', e.detail.value)
this.optionText.textContent = e.detail.text;
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: {
value: e.detail.value
}
}))
if (previousOption) {
previousOption.classList.remove('check-selected')
}
previousOption = e.target;
}
if (!e.detail.switching)
this.collapse()
e.target.classList.add('check-selected')
})
slot.addEventListener('slotchange', e => {
this.availableOptions = slot.assignedElements()
if (this.availableOptions[0]) {
this.availableOptions.forEach(option => {
if(option.hasAttribute('selected')){
this.optionText.textContent = option.textContent
this.setAttribute('value', option.getAttribute('value'))
option.classList.add('check-selected')
previousOption = option
}
})
}
});
document.addEventListener('mousedown', e => {
if (!this.contains(e.target) && this.isOpen) {
this.collapse()
}
})
}
})
// option
const smOption = document.createElement('template')
smOption.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
--color: inherit;
}
.option{
min-width: 100%;
padding: 0.8rem 1.2rem;
cursor: pointer;
overflow-wrap: break-word;
outline: none;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
color: var(--color);
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
:host(:focus){
outline: none;
background: rgba(var(--text-color), 0.1);
}
:host(:focus) .option .icon{
opacity: 0.4
}
:host(.check-selected) .icon{
opacity: 1 !important
}
.icon {
margin-right: 0.8rem;
fill: none;
height: 0.8rem;
width: 0.8rem;
stroke: rgba(var(--text-color), 0.7);
stroke-width: 10;
overflow: visible;
stroke-linecap: round;
border-radius: 1rem;
stroke-linejoin: round;
opacity: 0;
}
@media (hover: hover){
.option:hover{
background: rgba(var(--text-color), 0.1);
}
.option:hover .icon{
opacity: 0.4
}
}
</style>
<div class="option">
<svg class="icon" viewBox="0 0 64 64">
<polyline points="0.35 31.82 21.45 52.98 63.65 10.66"/>
</svg>
<slot></slot>
</div>`;
customElements.define('sm-option', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smOption.content.cloneNode(true))
}
sendDetails(switching) {
let optionSelected = new CustomEvent('optionSelected', {
bubbles: true,
composed: true,
detail: {
text: this.textContent,
value: this.getAttribute('value'),
switching: switching
}
})
this.dispatchEvent(optionSelected)
}
connectedCallback() {
let validKey = [
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight'
]
this.addEventListener('click', e => {
this.sendDetails()
})
this.addEventListener('keyup', e => {
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
this.sendDetails(false)
}
if (validKey.includes(e.code)) {
e.preventDefault()
this.sendDetails(true)
}
})
}
})
const smSwitch = document.createElement('template')
smSwitch.innerHTML = `
<style>
*{
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 0;
margin: 0;
}
:host{
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
}
label{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
width: 100%;
outline: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
:host(:not([disabled])) label:focus-visible{
-webkit-box-shadow: 0 0 0 0.1rem var(--accent-color);
box-shadow: 0 0 0 0.1rem var(--accent-color);
}
:host([disabled]) {
cursor: not-allowed;
opacity: 0.6;
pointer-events: none;
}
.switch {
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
width: 2.4rem;
flex-shrink: 0;
margin-left: auto;
padding: 0.2rem;
cursor: pointer;
border-radius: 2rem;
}
input {
display: none;
}
.track {
position: absolute;
left: 0;
right: 0;
height: 1.4rem;
-webkit-transition: background 0.3s;
-o-transition: background 0.3s;
transition: background 0.3s;
background: rgba(var(--text-color), 0.4);
-webkit-box-shadow: 0 0.1rem 0.3rem #00000040 inset;
box-shadow: 0 0.1rem 0.3rem #00000040 inset;
border-radius: 1rem;
}
.switch:active .button::after,
.switch:focus .button::after{
opacity: 1
}
.switch:focus-visible .button::after{
opacity: 1
}
.button::after{
content: '';
display: -webkit-box;
display: -ms-flexbox;
display: flex;
position: absolute;
height: 2.6rem;
width: 2.6rem;
background: rgba(var(--text-color), 0.2);
border-radius: 2rem;
opacity: 0;
-webkit-transition: opacity 0.3s;
-o-transition: opacity 0.3s;
transition: opacity 0.3s;
}
.button {
position: relative;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
height: 1rem;
width: 1rem;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
border-radius: 1rem;
-webkit-box-shadow: 0 0.1rem 0.4rem #00000060;
box-shadow: 0 0.1rem 0.4rem #00000060;
-webkit-transition: -webkit-transform 0.3s;
transition: -webkit-transform 0.3s;
-o-transition: transform 0.3s;
transition: transform 0.3s;
transition: transform 0.3s, -webkit-transform 0.3s;
border: solid 0.3rem white;
}
input:checked ~ .button {
-webkit-transform: translateX(100%);
-ms-transform: translateX(100%);
transform: translateX(100%);
}
input:checked ~ .track {
background: var(--accent-color);
}
</style>
<label tabindex="0">
<slot name="left"></slot>
<div part="switch" class="switch">
<input type="checkbox">
<div class="track"></div>
<div class="button"></div>
</div>
</label>`
customElements.define('sm-switch', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smSwitch.content.cloneNode(true))
this.switch = this.shadowRoot.querySelector('.switch');
this.input = this.shadowRoot.querySelector('input')
this.isChecked = false
this.isDisabled = false
}
static get observedAttributes() {
return ['disabled', 'checked']
}
get disabled() {
return this.isDisabled
}
set disabled(val) {
if (val) {
this.setAttribute('disabled', '')
} else {
this.removeAttribute('disabled')
}
}
get checked() {
return this.isChecked
}
set checked(value) {
if (value) {
this.setAttribute('checked', '')
} else {
this.removeAttribute('checked')
}
}
dispatch = () => {
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: {
value: this.isChecked,
checked: this.isChecked
}
}))
}
connectedCallback() {
this.addEventListener('keyup', e => {
if ((e.code === "Enter" || e.code === "Space") && !this.isDisabled) {
this.input.click()
}
})
this.input.addEventListener('click', e => {
if (this.input.checked)
this.checked = true
else
this.checked = false
this.dispatch()
})
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
if (name === 'disabled') {
if (this.hasAttribute('disabled')) {
this.disabled = true
}
else {
this.disabled = false
}
}
else if (name === 'checked') {
if (this.hasAttribute('checked')) {
this.isChecked = true
this.input.checked = true
}
else {
this.isChecked = false
this.input.checked = false
}
}
}
}
})
//notifications
const smNotifications = document.createElement('template')
smNotifications.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.hide{
opacity: 0 !important;
pointer-events: none !important;
}
.notification-panel{
display: -ms-grid;
display: grid;
width: 100%;
position: fixed;
top: 0;
right: 0;
z-index: 100;
max-height: 100%;
overflow: hidden auto;
-ms-scroll-chaining: none;
overscroll-behavior: contain;
}
.no-transformations{
-webkit-transform: none;
-ms-transform: none;
transform: none;
opacity: 1;
}
.notification-panel:empty{
display:none;
}
.notification{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
opacity: 0;
padding: 1rem 1.5rem;
margin-bottom: 0.5rem;
-webkit-transform: translateY(-1rem);
-ms-transform: translateY(-1rem);
transform: translateY(-1rem);
position: relative;
border-radius: 0.3rem;
-webkit-box-shadow: 0 0.1rem 0.2rem rgba(0, 0, 0, 0.1),
0.5rem 1rem 2rem rgba(0, 0, 0, 0.1);
box-shadow: 0 0.1rem 0.2rem rgba(0, 0, 0, 0.1),
0.5rem 1rem 2rem rgba(0, 0, 0, 0.1);
background: rgba(var(--foreground-color), 1);
-webkit-transition: height 0.3s, opacity 0.3s, -webkit-transform 0.3s;
transition: height 0.3s, opacity 0.3s, -webkit-transform 0.3s;
-o-transition: height 0.3s, transform 0.3s, opacity 0.3s;
transition: height 0.3s, transform 0.3s, opacity 0.3s;
transition: height 0.3s, transform 0.3s, opacity 0.3s, -webkit-transform 0.3s;
overflow: hidden;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
max-width: 100%;
touch-action: none;
}
h4:first-letter,
p:first-letter{
text-transform: uppercase;
}
h4{
font-weight: 400;
}
p{
line-height: 1.6;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
color: rgba(var(--text-color), 0.9);
overflow-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-all;
word-break: break-word;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
max-width: 100%;
}
.notification:last-of-type{
margin-bottom: 0;
}
.icon {
fill: none;
height: 1.6rem;
width: 1.6rem;
stroke: rgba(var(--text-color), 0.7);
overflow: visible;
stroke-linecap: round;
border-radius: 1rem;
stroke-linejoin: round;
cursor: pointer;
}
.error-icon{
stroke: #E53935;
}
.success-icon{
stroke: #00C853;
}
.close{
margin-left: 1rem;
padding: 0.5rem;
stroke-width: 10;
}
.notification-icon{
height: 1.4rem;
width: 1.4rem;
margin: 0.3em 1rem 0 0;
stroke-width: 6;
}
@media screen and (min-width: 640px){
.notification-panel{
max-width: 28rem;
width: max-content;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
}
.notification{
-ms-grid-column-align: end;
justify-self: end;
width: auto;
margin-right: 1.5rem;
margin-bottom: 1rem;
border-bottom: none;
border: solid 1px rgba(var(--text-color), 0.2);
-webkit-transform: translateX(1rem);
-ms-transform: translateX(1rem);
transform: translateX(1rem);
}
}
@media screen and (any-hover: none){
.close{
display: none
}
}
</style>
<div class="notification-panel"></div>
`
customElements.define('sm-notifications', class extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({
mode: 'open'
}).append(smNotifications.content.cloneNode(true))
}
handleTouchStart = (e) => {
this.notification = e.target.closest('.notification')
this.touchStartX = e.changedTouches[0].clientX
this.notification.style.transition = 'initial'
this.touchStartTime = e.timeStamp
}
handleTouchMove = (e) => {
e.preventDefault()
if (this.touchStartX < e.changedTouches[0].clientX) {
this.offset = e.changedTouches[0].clientX - this.touchStartX;
this.touchEndAnimataion = requestAnimationFrame(this.movePopup)
} else {
this.offset = -(this.touchStartX - e.changedTouches[0].clientX);
this.touchEndAnimataion = requestAnimationFrame(this.movePopup)
}
}
handleTouchEnd = (e) => {
this.notification.style.transition = 'transform 0.3s, opacity 0.3s'
this.touchEndTime = e.timeStamp
cancelAnimationFrame(this.touchEndAnimataion)
this.touchEndX = e.changedTouches[0].clientX
if (this.touchEndTime - this.touchStartTime > 200) {
if (this.touchEndX - this.touchStartX > this.threshold) {
this.removeNotification(this.notification)
} else if (this.touchStartX - this.touchEndX > this.threshold) {
this.removeNotification(this.notification, true)
} else {
this.resetPosition()
}
} else {
if (this.touchEndX > this.touchStartX) {
this.removeNotification(this.notification)
} else {
this.removeNotification(this.notification, true)
}
}
}
movePopup = () => {
this.notification.style.transform = `translateX(${this.offset}px)`
}
resetPosition = () => {
this.notification.style.transform = `translateX(0)`
}
push = (messageBody, type, pinned) => {
let notification = document.createElement('div'),
composition = ``
notification.classList.add('notification')
if (pinned)
notification.classList.add('pinned')
if (type === 'error') {
composition += `
<svg class="notification-icon icon error-icon" viewBox="0 0 64 64">
<path d="M32,4.73a3.17,3.17,0,0,1,2.76,1.59l13.9,24.09L62.57,54.49a3.19,3.19,0,0,1-2.76,4.78H4.19a3.19,3.19,0,0,1-2.76-4.78L15.34,30.41,29.24,6.32A3.17,3.17,0,0,1,32,4.73m0-1a4.14,4.14,0,0,0-3.62,2.09L14.47,29.91.57,54a4.19,4.19,0,0,0,3.62,6.28H59.81A4.19,4.19,0,0,0,63.43,54L49.53,29.91,35.62,5.82A4.14,4.14,0,0,0,32,3.73Z"/>
<line x1="32" y1="24" x2="32" y2="36"/>
<line x1="32" y1="46" x2="32" y2="48"/>
</svg>`
} else if (type === 'success') {
composition += `
<svg class="notification-icon icon success-icon" viewBox="0 0 64 64">
<polyline points="0.35 31.82 21.45 52.98 63.65 10.66"/>
</svg>`
}
composition += `
<p>${messageBody}</p>
<svg class="icon close" viewBox="0 0 64 64">
<title>Close</title>
<line x1="64" y1="0" x2="0" y2="64"/>
<line x1="64" y1="64" x2="0" y2="0"/>
</svg>`
notification.innerHTML = composition
this.notificationPanel.prepend(notification)
if (window.innerWidth > 640) {
notification.animate([{
transform: `translateX(1rem)`,
opacity: '0'
},
{
transform: 'translateX(0)',
opacity: '1'
}
], this.animationOptions).onfinish = () => {
notification.setAttribute('style', `transform: none;`);
}
} else {
notification.setAttribute('style', `transform: translateY(0); opacity: 1`)
}
notification.addEventListener('touchstart', this.handleTouchStart)
notification.addEventListener('touchmove', this.handleTouchMove)
notification.addEventListener('touchend', this.handleTouchEnd)
}
removeNotification = (notification, toLeft) => {
if (!this.offset)
this.offset = 0;
if (toLeft)
notification.animate([{
transform: `translateX(${this.offset}px)`,
opacity: '1'
},
{
transform: `translateX(-100%)`,
opacity: '0'
}
], this.animationOptions).onfinish = () => {
notification.remove()
}
else {
notification.animate([{
transform: `translateX(${this.offset}px)`,
opacity: '1'
},
{
transform: `translateX(100%)`,
opacity: '0'
}
], this.animationOptions).onfinish = () => {
notification.remove()
}
}
}
clearAll = () => {
Array.from(this.notificationPanel.children).forEach(child => {
this.removeNotification(child)
})
}
connectedCallback() {
this.notificationPanel = this.shadowRoot.querySelector('.notification-panel')
this.animationOptions = {
duration: 300,
fill: "forwards",
easing: "ease"
}
this.fontSize = Number(window.getComputedStyle(document.body).getPropertyValue('font-size').match(/\d+/)[0])
this.notification
this.offset
this.touchStartX = 0
this.touchEndX = 0
this.touchStartTime = 0
this.touchEndTime = 0
this.threshold = this.notificationPanel.getBoundingClientRect().width * 0.3
this.touchEndAnimataion;
this.notificationPanel.addEventListener('click', e => {
if (e.target.closest('.close'))(
this.removeNotification(e.target.closest('.notification'))
)
})
const observer = new MutationObserver(mutationList => {
mutationList.forEach(mutation => {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length) {
if (!mutation.addedNodes[0].classList.contains('pinned'))
setTimeout(() => {
this.removeNotification(mutation.addedNodes[0])
}, 5000);
if (window.innerWidth > 640)
this.notificationPanel.style.padding = '1.5rem 0 3rem 1.5rem';
else
this.notificationPanel.style.padding = '1rem 1rem 2rem 1rem';
} else if (mutation.removedNodes.length && !this.notificationPanel.children.length) {
this.notificationPanel.style.padding = 0;
}
}
})
})
observer.observe(this.notificationPanel, {
attributes: true,
childList: true,
subtree: true
})
}
})
// tab-header
const smTabHeader = document.createElement('template')
smTabHeader.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
--active-tab-color: rgba(var(--foreground-color), 1);
}
.tabs{
position: relative;
display: -ms-grid;
display: grid;
width: 100%;
}
.tab-header{
display: -ms-grid;
display: grid;
grid-auto-flow: column;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 1rem;
position: relative;
overflow: auto hidden;
max-width: 100%;
scrollbar-width: 0;
}
.indicator{
position: absolute;
left: 0;
bottom: 0;
height: 0.15rem;
border-radius: 1rem 1rem 0 0;
background: var(--accent-color);
-webkit-transition: width 0.3s, -webkit-transform 0.3s;
transition: width 0.3s, -webkit-transform 0.3s;
-o-transition: transform 0.3s, width 0.3s;
transition: transform 0.3s, width 0.3s;
transition: transform 0.3s, width 0.3s, -webkit-transform 0.3s;
pointer-events: none;
}
:host([variant="tab"]) .indicator{
height: 100%;
border-radius: 0.3rem;
}
:host(.round) .indicator{
border-radius: 3rem;
}
:host([variant="tab"]) .tab-header{
border-bottom: none;
}
.hide-completely{
display: none;
}
:host([variant="tab"]) .tab-header{
gap: 0.2rem;
display: -ms-inline-grid;
display: inline-grid;
justify-self: flex-start;
border-radius: 0.3rem;
}
:host([variant="tab"]) slot::slotted(.active){
color: var(--active-tab-color);
}
slot::slotted(.active){
color: var(--accent-color);
opacity: 1;
}
@media (hover: none){
.tab-header::-webkit-scrollbar-track {
-webkit-box-shadow: none !important;
background-color: transparent !important;
}
.tab-header::-webkit-scrollbar {
height: 0;
background-color: transparent;
}
}
</style>
<div part="tab-container" class="tabs">
<div part="tab-header" class="tab-header">
<slot></slot>
<div part="indicator" class="indicator"></div>
</div>
</div>
`;
customElements.define('sm-tab-header', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smTabHeader.content.cloneNode(true))
this.indicator = this.shadowRoot.querySelector('.indicator');
this.tabSlot = this.shadowRoot.querySelector('slot');
this.tabHeader = this.shadowRoot.querySelector('.tab-header');
}
sendDetails(element) {
this.dispatchEvent(
new CustomEvent("switchtab", {
bubbles: true,
detail: {
target: this.target,
rank: parseInt(element.dataset.rank)
}
})
)
}
moveIndiactor(tabDimensions) {
//if(this.isTab)
this.indicator.setAttribute('style', `width: ${tabDimensions.width}px; transform: translateX(${tabDimensions.left - this.tabHeader.getBoundingClientRect().left + this.tabHeader.scrollLeft}px)`)
//else
//this.indicator.setAttribute('style', `width: calc(${tabDimensions.width}px - 1.6rem); transform: translateX(calc(${ tabDimensions.left - this.tabHeader.getBoundingClientRect().left + this.tabHeader.scrollLeft}px + 0.8rem)`)
}
connectedCallback() {
if (!this.hasAttribute('target') || this.getAttribute('target').value === '') return;
this.prevTab
this.allTabs
this.activeTab
this.isTab = false
this.target = this.getAttribute('target')
if (this.hasAttribute('variant') && this.getAttribute('variant') === 'tab') {
this.isTab = true
}
this.tabSlot.addEventListener('slotchange', () => {
this.tabSlot.assignedElements().forEach((tab, index) => {
tab.dataset.rank = index
})
})
this.allTabs = this.tabSlot.assignedElements();
this.tabSlot.addEventListener('click', e => {
if (e.target === this.prevTab || !e.target.closest('sm-tab'))
return
if (this.prevTab)
this.prevTab.classList.remove('active')
e.target.classList.add('active')
e.target.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
})
this.moveIndiactor(e.target.getBoundingClientRect())
this.sendDetails(e.target)
this.prevTab = e.target;
this.activeTab = e.target;
})
let resizeObserver = new ResizeObserver(entries => {
entries.forEach((entry) => {
if (this.prevTab) {
let tabDimensions = this.activeTab.getBoundingClientRect();
this.moveIndiactor(tabDimensions)
}
})
})
resizeObserver.observe(this)
let observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.indicator.style.transition = 'none'
if (this.activeTab) {
let tabDimensions = this.activeTab.getBoundingClientRect();
this.moveIndiactor(tabDimensions)
} else {
this.allTabs[0].classList.add('active')
let tabDimensions = this.allTabs[0].getBoundingClientRect();
this.moveIndiactor(tabDimensions)
this.sendDetails(this.allTabs[0])
this.prevTab = this.tabSlot.assignedElements()[0];
this.activeTab = this.prevTab;
}
}
})
}, {
threshold: 1.0
})
observer.observe(this)
}
})
// tab
const smTab = document.createElement('template')
smTab.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
position: relative;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
z-index: 1;
}
.tab{
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
white-space: nowrap;
padding: 0.4rem 0.8rem;
font-weight: 500;
word-spacing: 0.1rem;
text-align: center;
-webkit-transition: color 0.3s;
-o-transition: color 0.3s;
transition: color 0.3s;
text-transform: capitalize;
height: 100%;
}
@media (hover: hover){
:host(.active) .tab{
opacity: 1;
}
.tab{
opacity: 0.7
}
.tab:hover{
opacity: 1
}
}
</style>
<div part="tab" class="tab">
<slot></slot>
</div>
`;
customElements.define('sm-tab', class extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({
mode: 'open'
}).append(smTab.content.cloneNode(true))
}
})
// tab-panels
const smTabPanels = document.createElement('template')
smTabPanels.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.panel-container{
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
width: 100%;
height: 100%;
overflow: hidden auto;
}
slot::slotted(.hide-completely){
display: none;
}
</style>
<div part="panel-container" class="panel-container">
<slot>Nothing to see here.</slot>
</div>
`;
customElements.define('sm-tab-panels', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smTabPanels.content.cloneNode(true))
this.panelSlot = this.shadowRoot.querySelector('slot');
}
connectedCallback() {
//animations
let flyInLeft = [{
opacity: 0,
transform: 'translateX(-1rem)'
},
{
opacity: 1,
transform: 'none'
}
],
flyInRight = [{
opacity: 0,
transform: 'translateX(1rem)'
},
{
opacity: 1,
transform: 'none'
}
],
flyOutLeft = [{
opacity: 1,
transform: 'none'
},
{
opacity: 0,
transform: 'translateX(-1rem)'
}
],
flyOutRight = [{
opacity: 1,
transform: 'none'
},
{
opacity: 0,
transform: 'translateX(1rem)'
}
],
animationOptions = {
duration: 300,
fill: 'forwards',
easing: 'ease'
}
this.prevPanel
this.allPanels
this.previousRank
this.panelSlot.addEventListener('slotchange', () => {
this.panelSlot.assignedElements().forEach((panel) => {
panel.classList.add('hide-completely')
})
})
this.allPanels = this.panelSlot.assignedElements()
this._targetBodyFlyRight = (targetBody) => {
targetBody.classList.remove('hide-completely')
targetBody.animate(flyInRight, animationOptions)
}
this._targetBodyFlyLeft = (targetBody) => {
targetBody.classList.remove('hide-completely')
targetBody.animate(flyInLeft, animationOptions)
}
document.addEventListener('switchtab', e => {
if (e.detail.target !== this.id)
return
if (this.prevPanel) {
let targetBody = this.allPanels[e.detail.rank],
currentBody = this.prevPanel;
if (this.previousRank < e.detail.rank) {
if (currentBody && !targetBody)
currentBody.animate(flyOutLeft, animationOptions).onfinish = () => {
currentBody.classList.add('hide-completely')
}
else if (targetBody && !currentBody) {
this._targetBodyFlyRight(targetBody)
} else if (currentBody && targetBody) {
currentBody.animate(flyOutLeft, animationOptions).onfinish = () => {
currentBody.classList.add('hide-completely')
this._targetBodyFlyRight(targetBody)
}
}
} else {
if (currentBody && !targetBody)
currentBody.animate(flyOutRight, animationOptions).onfinish = () => {
currentBody.classList.add('hide-completely')
}
else if (targetBody && !currentBody) {
this._targetBodyFlyLeft(targetBody)
} else if (currentBody && targetBody) {
currentBody.animate(flyOutRight, animationOptions).onfinish = () => {
currentBody.classList.add('hide-completely')
this._targetBodyFlyLeft(targetBody)
}
}
}
} else {
this.allPanels[e.detail.rank].classList.remove('hide-completely')
}
this.previousRank = e.detail.rank
this.prevPanel = this.allPanels[e.detail.rank];
})
}
})
const fundBlockTemplate = document.createElement('template')
fundBlockTemplate.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: 'Inter', sans-serif;
}
ul{
list-style: none;
}
a:any-link,
button{
position: relative;
display: inline-flex;
align-items: center;
background: none;
cursor: pointer;
outline: none;
color: inherit;
font-weight: 500;
font-size: 0.8rem;
border-radius: 0.3rem;
padding: 0.4rem 0.6rem;
align-self: flex-start;
text-decoration: none;
-webkit-tap-highlight-color: transparent;
border: 1px solid rgba(var(--text-color), 0.8);
}
a:any-link:focus-visible,
button:focus-visible{
outline: rgba(var(--text-color), 1) 0.1rem solid;
}
.flex{
display: flex;
}
.grid{
display: grid;
}
.gap-1{
gap: 1rem;
}
.gap-1-5{
gap: 1.5rem;
}
.flow-column{
grid-auto-flow: column;
}
.justify-start{
justify-content: flex-start;
}
.justify-end{
justify-content: flex-end;
}
.justify-right{
margin-left: auto;
}
.margin-bottom-0-5r{
margin-bottom: 0.5rem;
}
.margin-bottom-1-5r{
margin-bottom: 1.5rem;
}
.margin-bottom-3r{
margin-bottom: 3rem;
}
p {
font-size: 0.9rem;
max-width: 70ch;
line-height: 1.5;
color: rgba(var(--text-color), 0.8);
}
.hide-completely{
display: none !important;
}
.fund-block{
border-radius: 0.3rem;
padding: 1rem;
background-color: rgba(var(--foreground-color), 1);
box-shadow: 0 1rem 2rem -1rem rgba($color: #000000, $alpha: 0.16);
}
.fund-block__details {
gap: 2rem;
}
.fund-block__details > .grid:nth-of-type(1), .fund-block__details > .grid:nth-of-type(2) {
grid-template-columns: 1fr 1fr;
}
.tapout-container, .investment-container {
grid-column: 1/3;
}
.investment-container{
gap: 1.5rem;
justify-content: flex-start;
grid-template-columns: 1fr 1fr;
}
.tapout-list{
display: grid;
gap: 1.5rem;
overflow-x: auto;
grid-auto-flow: column;
justify-content: flex-start;
}
.tapout-list li{
min-width: 8rem;
}
.fund-link{
justify-self: flex-end;
}
.investor-group{
display: grid;
padding: 0.5rem;
margin-top: 1.8rem;
border-radius: 0.3rem;
box-shadow: 0 0 0 1px rgba(var(--text-color), 0.3);
}
.investor-group header{
margin-top: -1.5rem;
margin-bottom: 0.5rem;
justify-self: flex-end;
}
.investor-group > * {
background-color: rgba(var(--foreground-color), 1);
}
.investor-group h4{
padding: 0 0.5rem;
margin-left: -0.5rem;
}
.investor-group__list{
display: grid;
gap: 0.8rem;
grid-template-columns: minmax(0, 1fr);
}
.transaction-column{
display: flex;
flex-direction: column;
}
.label{
font-weight: 500;
font-size: 0.75rem;
margin-bottom: 0.2rem;
color: rgba(var(--text-color), 0.8);
}
.value{
font-weight: 600;
font-size: 0.9rem;
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;
}
@media only screen and (max-width: 640px) {
.fund-link{
grid-column: 2/3;
}
}
@media only screen and (min-width: 640px) {
.fund-block{
padding: 1.5rem;
}
.fund-block__details{
grid-template-columns: 1fr 1fr;
}
.investor-group{
padding: 0.5rem 1rem;
}
.justify-self-end{
justify-self: flex-end;
text-align: right;
}
.investment-container{
grid-template-columns: auto auto 1fr;
}
}
</style>
<section class="fund-block">
<div class="fund-block__details grid margin-bottom-3r">
<div class="grid flow-column gap-1">
<div class="grid">
<span class="label">Start date</span>
<span class="value start-date"></span>
</div>
<div class="grid">
<span class="label">End date</span>
<span class="value end-date"></span>
</div>
</div>
<div class="grid flow-column gap-1">
<div class="grid">
<span class="label">Initial BTC value</span>
<span class="value base-btc"></span>
</div>
<div class="grid">
<span class="label">Base USD rate</span>
<span class="value base-usd"></span>
</div>
</div>
<div class="tapout-container grid">
<span class="value margin-bottom-0-5r">Tapouts</span>
<ul class="tapout-list"></ul>
</div>
<div class="investment-container grid">
<div class="grid">
<span class="label">Total investment</span>
<span class="value total-investment"></span>
</div>
<div class="grid">
<span class="label">Total present value</span>
<span class="value net-value" style="color: var(--green)"></span>
</div>
<a class="fund-link" target="_blank">See fund on Blockchain</a>
</div>
</div>
<div class="grid">
<h4 class="margin-bottom-0-5r">Investors</h4>
<ul class="investors-group-list grid"></ul>
</div>
</section>
`
customElements.define('fund-block', class extends HTMLElement {
constructor() {
super()
this.shadow = this.attachShadow({
mode: 'open'
})
this.shadow.append(fundBlockTemplate.content.cloneNode(true))
this.totalInvestement = {}
this.totalNet = {}
}
get investors(){
return this.shadow.querySelectorAll('fund-investor')
}
get investorGroups(){
return this.shadow.querySelectorAll('.investor-group')
}
get investorGroupList(){
return this.shadow.querySelectorAll('.investor-group__list')
}
set data(obj){
const {
fundTxHref,
startDate,
endDate,
baseUsd,
baseBtc,
tapouts,
totalInvestment,
totalNet,
investorsFrag
} = obj
this.totalInvestment = totalInvestment
this.totalNet = totalNet
for (let item in this.totalInvestment){
this.totalInvestment[item] = parseFloat(this.totalInvestment[item])
}
for (let item in this.totalNet){
this.totalNet[item] = parseFloat(this.totalNet[item])
}
if(investorsFrag.childElementCount > 1)
this.shadow.querySelector('.fund-link').href = fundTxHref
else
this.shadow.querySelector('.fund-link').remove()
this.shadow.querySelector('.start-date').textContent = startDate
this.shadow.querySelector('.end-date').textContent = endDate
this.shadow.querySelector('.base-usd').textContent = `${baseUsd.toLocaleString(`en-US`, {style: 'currency', currency: 'INR'})}`
this.shadow.querySelector('.base-btc').textContent = `${baseBtc.toLocaleString(`en-US`, {style: 'currency', currency: 'USD'})}`
this.shadow.querySelector('.investors-group-list').append(investorsFrag)
this.toggleCurrency()
if(Object.keys(tapouts).length){
const tapoutsFrag = document.createDocumentFragment()
for(let tapout in tapouts){
const tapoutPoint = document.createElement('li')
tapoutPoint.classList.add('grid')
tapoutPoint.innerHTML = `
<span class="label">${tapout}</span>
<span class="value">${tapouts[tapout]}</span>
`
tapoutsFrag.append(tapoutPoint)
}
this.shadow.querySelector('.tapout-list').append(tapoutsFrag)
} else
this.shadow.querySelector('.tapout-container').remove()
}
toggleCurrency = () => {
this.shadow.querySelector('.total-investment').textContent = `${this.totalInvestment[preferredCurrency].toLocaleString(`en-${preferredCurrency.substring(0,2)}`, {style: 'currency', currency: preferredCurrency})}`
this.shadow.querySelector('.net-value').textContent = `${this.totalNet[preferredCurrency].toLocaleString(`en-${preferredCurrency.substring(0,2)}`, {style: 'currency', currency: preferredCurrency})}`
}
})
const render = {
investorInput(){
const investorInput = document.createElement('li')
investorInput.classList.add('investor-input', 'grid')
investorInput.innerHTML = `
<sm-input placeholder="FLO ID" class="outlined" animate></sm-input>
<sm-input placeholder="Amount(₹)" type="number" min=0 class="outlined" animate></sm-input>
<button class="remove-investor" title="Remove this investor">
<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 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
</button>
`
return investorInput
},
fundPlaceholder(){
const fund_ph = document.createElement('li')
fund_ph.classList.add('fund-placeholder', 'grid')
fund_ph.innerHTML = `
<div class="flex-grid justify-start">
<div class="placeholder__block"></div>
<div class="placeholder__block"></div>
<div class="placeholder__block"></div>
<div class="placeholder__block"></div>
</div>
<div class="grid flow-column gap-1">
<div class="placeholder__block"></div>
<div class="placeholder__block"></div>
</div>
<div class="placeholder__block justify-self-end"></div>
`
return fund_ph
}
}
const domRefs = {};
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;
}
}
}
//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",
"",
true
);
window.addEventListener("offline", () => {
notify(
"There seems to be a problem connecting to the internet, Please check you internet connection.",
"error",
true,
true
);
});
window.addEventListener("online", () => {
getRef("notification_drawer").clearAll();
notify("We are back online.", "success");
});
const themeSwitcher = getRef("theme_switcher");
if (themeSwitcher) {
if (localStorage.theme === "dark") {
nightlight();
themeSwitcher.checked = true;
} else {
daylight();
themeSwitcher.checked = false;
}
function daylight() {
document.body.setAttribute("data-theme", "light");
}
function nightlight() {
document.body.setAttribute("data-theme", "dark");
}
themeSwitcher.addEventListener("change", function (e) {
if (this.checked) {
nightlight();
localStorage.setItem("theme", "dark");
} else {
daylight();
localStorage.setItem("theme", "light");
}
});
}
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
function notify(message, mode, pinned, sound) {
if (mode === "error") console.error(message);
else console.log(message);
getRef("notification_drawer").push(message, mode, pinned);
if (navigator.onLine && sound) {
getRef("notification_sound").currentTime = 0;
getRef("notification_sound").play();
}
}
window.addEventListener("load", () => {
document.addEventListener("pointerdown", (e) => {
if (e.target.closest("button, sm-button:not([disable]), .interact")) {
createRipple(e, e.target.closest("button, sm-button, .interact"));
}
else if(isDropdownOpen && !e.target.closest('.dropdown')){
changeDropdownState('profile_dropdown', 'hide')
}
});
});
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();
};
}
let timerId;
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 preferredCurrency
if(localStorage.getItem('preferred-currency')){
preferredCurrency = localStorage.getItem('preferred-currency')
document.querySelector(`sm-option[value="${localStorage.getItem('preferred-currency')}"]`).setAttribute('selected', '')
}
else{
preferredCurrency = 'inr'
localStorage.setItem('preferred-currency', 'inr')
getRef('currency_selector').value = 'INR'
document.querySelector(`sm-option[value="inr"]`).setAttribute('selected', '')
}
getRef('currency_selector').addEventListener('change', e => {
preferredCurrency = e.detail.value
localStorage.setItem('preferred-currency', e.detail.value)
document.querySelectorAll('fund-block').forEach(fund => fund.toggleCurrency())
})
let isDropdownOpen = false
function changeDropdownState(target, mode, trigger){
const options = {
duration: 300,
easing: 'ease',
fill: 'both',
}
if(mode === 'show'){
showDropdown(target, options, trigger)
}
else if(mode === 'toggle'){
if(isDropdownOpen){
hideDropdown(target, options)
}
else{
showDropdown(target, options, trigger)
}
}
else if (mode === 'hide'){
hideDropdown(target, options)
}
}
function showDropdown(target, options, trigger){
if(isDropdownOpen) return
if(trigger){
const triggerDimensions = trigger.getBoundingClientRect()
getRef(target).setAttribute('style', `top: ${triggerDimensions.top + triggerDimensions.height + document.documentElement.scrollTop}px; right: calc(${window.innerWidth - triggerDimensions.right}px - 1.5rem)`)
}
getRef(target).classList.remove('hide-completely')
getRef(target).animate([
{transform: 'translateY(-1rem)', opacity: 0},
{transform: 'translateY(0)', opacity: 1},
], options)
.onfinish = () => {
isDropdownOpen = true
}
}
function hideDropdown(target, options){
if(!isDropdownOpen) return
getRef(target).animate([
{transform: 'translateY(0)', opacity: 1},
{transform: 'translateY(-1rem)', opacity: 0},
], options)
.onfinish = () => {
isDropdownOpen = false
getRef(target).classList.add('hide-completely')
}
}
function showPage(target){
document.querySelector('.page:not(.hide-completely)')?.classList.add('hide-completely')
getRef(target).classList.remove('hide-completely')
if(target === 'home_page'){
clearAddedInvestors()
getRef("create_fund_form").reset()
if(window.location.hash !== ''){
getRef(`${window.location.hash.split('#').pop()}`).scrollIntoView({behavior: 'smooth', block: 'start'})
}
}
if(target !== 'confirm_term_page'){
getRef('get_term_private_key').value = ''
getRef('get_fund_private_key').value = ''
}
if(target === 'loading_page' || target === 'error_page'){
getRef('main_header').classList.add('hide-completely')
}
else{
getRef('main_header').classList.remove('hide-completely')
}
}
getRef('search_investor').addEventListener('input', e => {
throttle(() => {
document.querySelectorAll('fund-block').forEach(block => {
block.investors.forEach( child => {
if(child.dataset.floId.toLowerCase().includes(getRef('search_investor').value.trim().toLowerCase())){
child.classList.remove('hide-completely')
}
else{
child.classList.add('hide-completely')
}
})
block.investorGroupList.forEach(group => {
if(Array.from(group.children).every(elem => elem.classList.contains('hide-completely'))){
group.parentNode.classList.add('hide-completely')
}
else{
group.parentNode.classList.remove('hide-completely')
}
})
if(Array.from(block.investorGroups).every(elem => elem.classList.contains('hide-completely'))){
block.classList.add('hide-completely')
}
else{
block.classList.remove('hide-completely')
}
if(Array.from(getRef('fund_list').children).every(elem => elem.classList.contains('hide-completely'))){
getRef('fund_list__empty-state').classList.remove('hide-completely')
}
else{
getRef('fund_list__empty-state').classList.add('hide-completely')
}
})
}, 100)
})
getRef('investors_input_list').addEventListener('click', e => {
if(e.target.closest('.remove-investor')){
e.target.closest('.investor-input').remove()
}
})
function renderInvestor(){
getRef('investors_input_list').append(render.investorInput())
getRef('investors_input_list').lastElementChild.scrollIntoView()
getRef('investors_input_list').lastElementChild.children[0].focusIn()
}
function clearAddedInvestors(){
getRef('investors_input_list').innerHTML = ``
getRef('investors_input_list').append(render.investorInput())
}
function getAddedInvestors(){
const allInvestorsInput = getRef('investors_input_list').querySelectorAll('.investor-input')
const addedInvestors = []
allInvestorsInput.forEach(investor => {
const floId = investor.children[0].value.trim()
const amount = investor.children[1].value.trim()
addedInvestors.push([floId, amount])
})
return addedInvestors
}
getRef('fund_creation_toggle').addEventListener('change', e => {
if(e.detail.checked){
getRef('fund_details_form').classList.add('hide-completely')
getRef('fund_details_form').querySelectorAll('input').forEach(input => input.disabled = true)
getRef('create_fund_button').classList.add('hide-completely')
getRef('add_investors_button').classList.remove('hide-completely')
getRef('fund_selector_container').classList.remove('hide-completely')
}
else{
getRef('fund_details_form').classList.remove('hide-completely')
getRef('create_fund_button').classList.remove('hide-completely')
getRef('fund_details_form').querySelectorAll('input').forEach(input => input.disabled = false)
getRef('add_investors_button').classList.add('hide-completely')
getRef('fund_selector_container').classList.add('hide-completely')
}
})
getRef('tapout_toggle').addEventListener('change', e => {
if(!e.detail.checked){
getRef('tapout_container').classList.add('hide-completely')
getRef('tapout_container').querySelectorAll('input').forEach(input => input.disabled = true)
}
else{
getRef('tapout_container').classList.remove('hide-completely')
getRef('tapout_container').querySelectorAll('input').forEach(input => input.disabled = false)
}
})
/* function renderfundPlaceholder(){
getRef('fund_list').innerHTML = ``
const frag = document.createDocumentFragment()
for (let index = 0; index < 4; index++) {
frag.append(render.fundPlaceholder())
}
getRef('fund_list').append(frag)
} */
/* getRef('term_selector').addEventListener('change', e => {
const floID = e.detail.value
getRef('fund_selector').querySelectorAll('.fund-option').forEach(option => option.classList.add('hide-completely'))
getRef('fund_selector').querySelector(`.fund-option[data-flo-id="${floID}"]`)?.classList.remove('hide-completely')
}) */
getRef('refresh_button').addEventListener("click", refresh);
var USD_current, BTC_current;
function refresh(showLoader = true) {
if (showLoader)
showPage('loading_page')
getCurrentRates().then(async (rates) => {
USD_current = rates.USD_INR;
BTC_current = rates.BTC_USD;
console.log(`USD rate: ${USD_current} INR\nBTC rate: ${BTC_current} USD`);
getRef("usd-rate").textContent = `USD: ₹${rates.USD_INR.toFixed(2)}`;
getRef("btc-usd-rate").textContent = `BTC: ${parseFloat(rates.BTC_USD.toFixed(2)).toLocaleString(`en-US`, { style: 'currency', currency: 'USD' })}`;
getRef('fund_list').innerHTML = '';
getRef('fund_selector').innerHTML = ''
//getRef('term_selector').innerHTML = ''
refreshBlockchainData().then(funds => {
renderFunds(funds)
showPage('home_page')
}).catch(error => {
console.error(error)
showPage('error_page')
})
}).catch(error => console.error(error))
}
function renderFunds(funds) {
if (!Object.keys(funds).length)
return;
const removeElementIfExist = id => {
let existing = document.getElementById(id);
if (existing)
existing.remove();
}
const fundsFrag = document.createDocumentFragment()
let sortArray = [];
for (let k in funds) {
let f = bobsFund.parse(funds[k].map(a => a.data));
console.info(f);
let startDate = new Date(f.start_date).getTime()
const tapouts = {}
if (f.tapoutInterval)
f.tapoutInterval.forEach((i, k) => {
let ts = bobsFund.dateAdder(startDate, i),
te = bobsFund.dateAdder(ts, f.topoutWindow);
tapouts[`Tapout ${k + 1}`] = `${bobsFund.dateFormat(ts)} to ${bobsFund.dateFormat(te)}`
})
const fundObj = {
//termTxHref: `${floBlockchainAPI.current_server}tx/${term.txid}`,
fundTxHref: `${floBlockchainAPI.current_server}tx/${funds[k][0].txid}`,
startDate: bobsFund.dateFormat(f.start_date),
endDate: bobsFund.dateFormat(bobsFund.dateAdder(startDate, f.duration)),
baseUsd: f.USD_base,
baseBtc: f.BTC_base,
tapouts,
}
// Creating fund selection options
const smOption = document.createElement('sm-option')
smOption.innerHTML = `
<div class="grid gap-0-5">
<span>Start date: ${fundObj.startDate}</span>
<span>Base BTC: ${parseFloat(fundObj.baseBtc).toLocaleString(`en-US`, {style: 'currency', currency: 'USD'})} | Base USD: ₹${fundObj.baseUsd}</span>
</div>
`
smOption.setAttribute('value', funds[k][0].txid)
fundsFrag.append(smOption)
const investorsFrag = document.createDocumentFragment()
let total_invested = total_net = 0;
for(let investor in f.investments){
const investorGroup = document.createElement('li')
investorGroup.classList.add('investor-group')
investorGroup.innerHTML = `
<header class="flex align-center">
<a class="tx-link justify-right" href="${floBlockchainAPI.current_server}tx/${funds[k][f.investments[investor].i].txid}" target="_blank">See transaction on Blockchain</a>
</header>
<ul class="investor-group__list"></ul>
`;
let amount = f.investments[investor].amount,
netVal = bobsFund.calcNetValue(f.BTC_base, BTC_current, f.USD_base, USD_current, amount, f.fee);
console.info(investor, amount, netVal);
const obj = {
floId: investor,
amountInvested: {
inr: amount.toFixed(2),
usd: (amount / f.USD_base).toFixed(2),
},
netValue: {
inr: netVal.toFixed(2),
usd: (netVal / USD_current).toFixed(2),
}
}
total_invested += amount;
total_net += netVal;
const investorCard = document.createElement('fund-investor')
investorCard.data = obj
if(f.investments[investor].closed){
/* TODO: UI: render closing data
if closed, netVal shoud not be displayed
f.investments[investor].closed -> Object {
BTC_net: BTC value at closing date
endDate: closing date
amountFinal: final amount (withdrawn)
payment_refRef: if amount is withdrawn via token system, txid of the token transfer
USD_net: USD value at closing date
refSign: signature of the inverstor for closing (hidden)
i: index of the txid (of closing tx) ie, funds[k][i].txid
}
*/
}
investorGroup.querySelector('.investor-group__list').append(investorCard)
investorsFrag.append(investorGroup)
}
fundObj.totalInvestment = {
inr: total_invested.toFixed(2),
usd: (total_invested / f.USD_base).toFixed(2)
}
fundObj.totalNet = {
inr: total_net.toFixed(2),
usd: (total_net / USD_current).toFixed(2)
}
fundObj.investorsFrag = investorsFrag
const fundBlock = document.createElement('fund-block')
fundBlock.data = fundObj
removeElementIfExist(k);
fundBlock.id = k;
let i = sortArray.length - 1;
while (i >= 0 && sortArray[i] < startDate) { //[9, 7, 5, 3]
sortArray[i + 1] = sortArray[i]
i--;
}
sortArray[i + 1] = startDate;
let list = getRef('fund_list')
list.insertBefore(fundBlock, list.childNodes[i + 1]);
}
//const fundGroup = document.createElement('section')
//fundGroup.dataset.floId = term.floID
//fundGroup.classList.add('fund-option')
//fundGroup.append(fundsFrag)
getRef('fund_selector').append(fundsFrag)
}
/* getRef("create_term_form").addEventListener("submit", evt => {
evt.preventDefault();
let f = evt.target;
let tap_it = getRef("tap_it").value
let tapoutInterval = f["tap_iv"].value.split(",").map(v => v.trim() + " " + tap_it);
let termStr = createTermString(f["floid"].value, f["max_pv"].value + " " + getRef("max_pt").value, f["tap_wv"].value + " " + getRef("tap_wt").value, tapoutInterval)
console.log(termStr)
getRef('term_details').innerHTML = termStr.replace(/\|/g, "<br>")
getRef('term_admin_id').innerHTML = `Enter Private key of adminID <h5 class="weight-400">${floGlobals.adminID}</h5>`
showPage('confirm_term_page')
getRef('create_term_button').onclick = () => {
const privKey = getRef('get_term_private_key').value
if (!floCrypto.verifyPrivKey(privKey, floGlobals.adminID)) {
notify("Access Denied! incorrect private key", 'error');
return
}
floBlockchainAPI.writeData(floGlobals.adminID, termStr, privKey, f["floid"].value).then(result => {
console.log(result);
showPage('admin_page')
notify("Term added in blockchain", 'success');
getRef("create_term_form").reset()
refresh(false)
}).catch(error => console.error(error))
}
}) */
getRef("create_fund_form").addEventListener("submit", evt => {
evt.preventDefault(); //tapout_toggle
let f, fStr, duration, tapoutWindow, tapoutInterval, investments;
f = evt.target
duration = f["max_pv"].value + " " + getRef("max_pt").value
if (!getRef("tapout_toggle").checked)
tapoutWindow = tapoutInterval = null
else {
let tap_it = getRef("tap_it").value
tapoutInterval = f["tap_iv"].value.split(",").map(v => v.trim() + " " + tap_it)
tapoutWindow = f["tap_wv"].value + " " + getRef("tap_wt").value
}
investments = getAddedInvestors()
let createMod = !getRef("fund_creation_toggle").checked;
console.info(investments)
if (createMod) //create new fund
fStr = bobsFund.stringify.main(f["btc_base"].value, f["usd_rate"].value, f["start_date"].value, duration, investments, f["fee"].value, tapoutWindow, tapoutInterval);
else //add investments to existing fund
fStr = bobsFund.stringify.continue(getRef("fund_selector").value, investments);
console.log(fStr);
if (fStr.length >= 1040) {
console.error("flo data length is too long, Please reduce it and try again");
notify("floData is too large! Please reduce it and try again.", 'error');
return
}
getRef('fund_details').innerHTML = fStr.replace(/\|/g, "<br>")
getRef('fund_admin_id').innerHTML = `Enter Private key of admin ID <h5 class="weight-400">${floGlobals.adminID}</h5>`
showPage('confirm_fund_page')
getRef('confirm_fund_button').onclick = () => {
const privKey = getRef('get_fund_private_key').value
console.log(privKey)
if (!floCrypto.verifyPrivKey(privKey, floGlobals.adminID)) {
notify("Access Denied! incorrect private key", 'error');
return
}
floBlockchainAPI.writeData(floGlobals.adminID, fStr, privKey, floGlobals.adminID).then(result => {
console.log(result);
showPage('admin_page')
notify(createMod ? "New Fund created" : "Invesments added to fund", 'success');
getRef("create_fund_form").reset()
clearAddedInvestors()
refresh(false)
}).catch(error => console.error(error))
}
})
</script>
</body>
</html>