ribcpayments/js/main_UI.js
2022-12-08 12:17:45 +05:30

440 lines
15 KiB
JavaScript

"use strict";
// Global variables
const appPages = ['dashboard', 'settings'];
// Global variables
const { html, render: renderElem } = uhtml;
//Checks for internet connection status
if (!navigator.onLine)
floGlobals.connectionErrorNotification = notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error')
window.addEventListener('offline', () => {
floGlobals.connectionErrorNotification = notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error')
})
window.addEventListener('online', () => {
getRef('notification_drawer').remove(floGlobals.connectionErrorNotification)
notify('We are back online.', 'success')
})
// Use instead of document.getElementById
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;
}
}
}
// Use when a function needs to be executed after user finishes changes
const debounce = (callback, wait) => {
let timeoutId = null;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback.apply(null, args);
}, wait);
};
}
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
function notify(message, mode, options = {}) {
let icon
switch (mode) {
case 'success':
icon = `<svg class="icon icon--success" 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 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>`
break;
case 'error':
icon = `<svg class="icon 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>`
options.pinned = true
break;
}
if (mode === 'error') {
console.error(message)
}
return getRef("notification_drawer").push(message, { icon, ...options });
}
function getFormattedTime(timestamp, format) {
try {
timestamp = parseInt(timestamp)
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
let finalHours = ``;
if (hours > 12)
finalHours = `${hours - 12}:${minutes}`
else if (hours === 0)
finalHours = `12:${minutes}`
else
finalHours = `${hours}:${minutes}`
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`
switch (format) {
case 'date-only':
return `${month} ${date}, ${year}`;
break;
case 'time-only':
return finalHours;
case 'relative':
return relativeTime.from(timestamp)
default:
return `${month} ${date}, ${year} at ${finalHours}`;
}
} catch (e) {
console.error(e);
return timestamp;
}
}
window.addEventListener('hashchange', e => routeTo(window.location.hash))
window.addEventListener("load", () => {
document.body.classList.remove('hidden')
document.addEventListener("pointerdown", (e) => {
if (e.target.closest("button, .interact")) {
createRipple(e, e.target.closest("button, .interact"));
}
});
document.addEventListener('copy', () => {
notify('copied', 'success')
})
document.addEventListener('keydown', e => {
if (e.key === '/') {
e.preventDefault();
getRef('search_payments').focusIn()
}
})
getRef('search_payments').addEventListener('input', e => {
const searchQuery = e.target.value.toLowerCase();
const filteredInterns = []
for (const floId in floGlobals.internTxs) {
if (floId.toLowerCase().includes(searchQuery) || floGlobals.appObjects.RIBC.internList[floId].toLowerCase().includes(searchQuery))
filteredInterns.push({ floId, name: floGlobals.appObjects.RIBC.internList[floId] })
}
// sort filtered by relevance
filteredInterns.sort((a, b) => {
if (a.floId.toLowerCase().includes(searchQuery) && b.floId.toLowerCase().includes(searchQuery)) {
return a.name.toLowerCase().includes(searchQuery) ? -1 : 1
} else if (a.floId.toLowerCase().includes(searchQuery)) {
return -1
} else if (b.floId.toLowerCase().includes(searchQuery)) {
return 1
} else {
return a.name.toLowerCase().includes(searchQuery) ? -1 : 1
}
})
renderElem(getRef("intern_payment_list"), html`${filteredInterns.map(intern => render.internCard(intern.floId))}`);
})
});
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();
};
}
const appState = {
params: {},
}
function routeTo(targetPage) {
const routingAnimation = { in: slideInUp, out: slideOutUp }
let pageId
let subPageId1
let searchParams
let params
if (targetPage === '') {
pageId = 'home'
history.replaceState(null, null, '#/home');
} else {
if (targetPage.includes('/')) {
if (targetPage.includes('?')) {
const splitAddress = targetPage.split('?')
searchParams = splitAddress.pop();
[, pageId, subPageId1] = splitAddress.pop().split('/')
} else {
[, pageId, subPageId1] = targetPage.split('/')
}
} else {
pageId = targetPage
}
}
if (!getRef(pageId)?.classList.contains('page')) return
appState.currentPage = pageId
if (searchParams) {
const urlSearchParams = new URLSearchParams('?' + searchParams);
params = Object.fromEntries(urlSearchParams.entries());
}
if (params)
appState.params = params
switch (pageId) {
case 'intern':
if (params && params.id) {
render.intern(params.id)
}
break;
}
switch (appState.lastPage) {
case 'intern':
routingAnimation.in = slideInRight;
routingAnimation.out = slideOutRight;
break;
}
switch (pageId) {
case 'intern':
routingAnimation.in = slideInLeft;
routingAnimation.out = slideOutLeft;
break;
}
if (appState.lastPage !== pageId) {
document.querySelectorAll('.page').forEach(page => page.classList.add('hidden'))
getRef(pageId).closest('.page').classList.remove('hidden')
if (appState.lastPage) {
getRef(appState.lastPage).animate(routingAnimation.out, { duration: floGlobals.prefersReducedMotion ? 0 : 150, fill: 'forwards', easing: 'ease' }).onfinish = (e) => {
e.target.effect.target.classList.add('hidden')
}
}
getRef(pageId).classList.remove('hidden')
getRef(pageId).animate(routingAnimation.in, { duration: floGlobals.prefersReducedMotion ? 0 : 150, fill: 'forwards', easing: 'ease' }).onfinish = (e) => {
appState.lastPage = pageId
}
}
}
const slideInLeft = [
{
opacity: 0,
transform: 'translateX(1rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutLeft = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(-1rem)'
},
]
const slideInRight = [
{
opacity: 0,
transform: 'translateX(-1rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutRight = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(1rem)'
},
]
const slideInDown = [
{
opacity: 0,
transform: 'translateY(-1rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutDown = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(1rem)'
},
]
const slideInUp = [
{
opacity: 0,
transform: 'translateY(1rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutUp = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(-1rem)'
},
]
floGlobals.internTxs = {}
function formatAmount(amount = 0) {
if (!amount)
return '₹0';
return amount.toLocaleString(`en-IN`, { style: 'currency', currency: 'inr' })
}
function fetchRibcData() {
return floCloudAPI.requestObjectData("RIBC", {
application: "InternManage",
receiverID: "FMyRTrz9CG4TFNM6rCQgy3VQ5NF23bY2xD"
});
}
function fetchInternData() {
return floBlockchainAPI
.readAllTxs("FThgnJLcuStugLc24FJQggmp2WgaZjrBSn")
.then((allTxs) => {
allTxs.forEach((tx) => {
const floId = tx.vout[0].scriptPubKey.addresses[0];
if (!floGlobals.appObjects.RIBC.internList[floId]) return; // not an intern
const { txid, floData, time } = tx
if (!floGlobals.internTxs[floId])
floGlobals.internTxs[floId] = {
total: 0,
txs: []
};
const amount = parseFloat(floData.match(/([0-9]+)/)[1]) || 0; // get amount from floData
floGlobals.internTxs[floId].total += amount;
floGlobals.internTxs[floId].txs.push({
txid,
amount,
time
});
});
render.internPaymentList();
}).catch((err) => {
console.log(err);
});
}
const render = {
internCard(floId) {
const { total, txs } = floGlobals.internTxs[floId];
return html`
<li>
<a href=${`#/intern?id=${floId}`} class="intern-card">
<div class="flex flex-direction-column gap-0-5">
<h3>${floGlobals.appObjects.RIBC.internList[floId]}</h3>
<sm-copy value=${floId}></sm-copy>
</div>
<div class="flex flex-direction-column">
<p>Last payment: <b>${formatAmount(txs[0].amount)}</b> on ${getFormattedTime(txs[0].time, 'date-only')}</p>
<p>Total paid: <b>${formatAmount(total)}</b></p>
</div>
<button class="button button--small button--colored margin-left-auto">
See details
<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="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z"/></svg>
</button>
</a>
</li>
`;
},
internPaymentList() {
const renderedList = []
for (const intern in floGlobals.internTxs) {
renderedList.push(render.internCard(intern));
}
renderElem(getRef("intern_payment_list"), html`${renderedList}`);
},
paymentCard(tx) {
const { txid, amount, time } = tx;
return html`
<li class="payment-card">
<time>${getFormattedTime(time, 'date-only')}</time>
<div class="flex align-items-center space-between">
<p class="amount">${formatAmount(amount)}</p>
<a class="button button--small button--colored" href=${`https://flosight.duckdns.org/tx/${txid}`} target="_blank"
rel="noopener noreferrer">
<svg class="icon margin-right-0-5" 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> <path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"> </path>
</svg>
View transaction
</a>
</div>
</li>
`;
},
intern(floId) {
renderElem(getRef('intern'), html`
<a href="#/home" class="button button--colored margin-right-auto back-button">
<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" opacity=".87"/><path d="M17.51 3.87L15.73 2.1 5.84 12l9.9 9.9 1.77-1.77L9.38 12l8.13-8.13z"/></svg>
Back
</a>
<section id="intern__details" class="flex flex-direction-column gap-1">
<h1>${floGlobals.appObjects.RIBC.internList[floId]}</h1>
<div>
<p style="font-size: 0.9rem;">FLO Address</p>
<sm-copy value=${floId}></sm-copy>
</div>
<p>Total paid: <b>${formatAmount(floGlobals.internTxs[floId].total)}</b></p>
</section>
<section class="flex flex-direction-column gap-1">
<h4>Payment history</h4>
<ul id="payment_history">
${floGlobals.internTxs[floId].txs.map(tx => render.paymentCard(tx))}
</ul>
</section>
`)
}
}
async function main() {
try {
await fetchRibcData();
await fetchInternData()
routeTo(window.location.hash)
} catch (err) {
console.log(err);
}
}