flologsheet/index.html
sairajzero 3624cb04c2 Improvements and Move to new-cloud
- Now sheets have id (random floID) as identifier instead of title
- Fixes for the same
2022-09-15 01:58:28 +05:30

1422 lines
62 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FLO LogSheet</title>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800;900&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="css/main.css">
<script id="floGlobals">
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
const floGlobals = {
//Required for all
blockchain: "FLO",
adminID: "FKAEdnPfjXLHSYwrXQu377ugN4tXU7VGdf", //"FRaBr5F665RVkQ1A1EYrMfbX2UF52vWjKr",
application: "TEST_MODE", // "LogSheet",
// for storing signle log sheet data
currentSheet: {}
}
</script>
<script src="scripts/lib.js"></script>
<script src="scripts/floCrypto.js"></script>
<script src="scripts/floBlockchainAPI.js"></script>
<script src="scripts/compactIDB.js"></script>
<script src="scripts/floCloudAPI.js"></script>
<script src="scripts/floDapps.js"></script>
<script src="scripts/logsheet.js"></script>
<script id="onLoadStartUp">
function onLoadStartUp() {
//display loading screen
showLoader()
//document.getElementById("loading-screen").classList.remove("hide")
//clear Rendered Elements
/*clearElement(document.getElementById('person-list'))*/
clearElement(sheetsContainer)
//set the custom Privkey input
floDapps.setCustomPrivKeyInput(() => {
return new Promise((resolve, reject) => {
hideLoader()
showPage('sign_in_page')
let signInBtn = document.getElementById('sign_in_btn'),
//guestButton = document.getElementById('guest_btn'),
privateKeyInput = document.getElementById('get_priv_key_field')
signInBtn.onclick = () => {
hidePopup()
showLoader()
resolve(privateKeyInput.value)
privateKeyInput.value = ''
}
/*guestButton.onclick = () => {
hidePopup()
showLoader()
reject(null)
}*/
})
})
//invoke the startup functions
floDapps.launchStartUp().then(result => {
console.log(result)
document.getElementById("user_flo_id").textContent = myFloID
reactor.dispatchEvent("startUpSuccessLog", `Downloading objectData! Please Wait...`)
//request object data from Supernode
logSheet.init().then(result => {
console.log(result)
//Render the personList and sheetList
renderPersonList(logSheet.listPersons())
renderSheetList(logSheet.listSheets())
//hide loading screen
hideLoader()
showPage('home_page')
//display add buttons if subAdmin, else hide
if (floGlobals.subAdmins.includes(myFloID)) {
document.querySelectorAll('.sub-admin-option').forEach(option => option.classList.remove('hide-completely'))
} else {
document.querySelectorAll('.sub-admin-option').forEach(option => option.classList.add('hide-completely'))
}
}).catch(error => {
reactor.dispatchEvent("startUpErrorLog", `Failed to download objectData`)
notify(error, "error")
})
}).catch(error => notify(error, "error"))
}
</script>
</head>
<body onload="onLoadStartUp()" class="hide-completely">
<audio id="notification_sound">
<source src="https://rmservices.duckdns.org/files/notification-sound.mp3" type="audio/mpeg">
<source src="https://rmservices.duckdns.org/files/notification-sound.ogg" type="audio/ogg">
</audio>
<sm-popup id="confirmation">
<p id="confirm_message"></p>
<div class="flex align-center">
<sm-button variant="no-outline" class="cancel-btn">Cancel</sm-button>
<sm-button variant="no-outline" class="submit-btn">OK</button>
</div>
</sm-popup>
<sm-popup id="prompt">
<h4 id="prompt_message">Some input required</h4>
<sm-input id="prompt_field"></sm-input>
<div class="flex align-center">
<sm-button variant="no-outline" class="cancel-btn">Cancel</sm-button>
<sm-button variant="no-outline" class="submit-btn">OK</button>
</div>
</sm-popup>
<sm-notifications id="show_message"></sm-notifications>
<!-- Loading screen-->
<section id="main_loader" class="grid">
<svg id="loader" viewBox="0 0 73 73">
<title>Loader</title>
<path d="M72.5,36.5c0,19.88-16.12,36-36,36s-36-16.12-36-36s16.12-36,36-36S72.5,16.62,72.5,36.5" />
</svg>
<h4 id="tip_container">Loading RanchiMall FLO LogSheet</h4>
<sm-button onclick="signOut()">Sign Out</sm-button>
</section>
<!-- User settings popup -->
<sm-popup id="user_popup">
<header class="popup-header" slot="header">
<svg class="icon" onclick="this.closest('sm-popup').hide()" 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>
<h4>
Settings
</h4>
</header>
<section>
<h4>My FLO address</h4>
<div class="copy-row">
<h4 id="user_flo_id" class="copy"></h4>
<svg class="icon" onclick="copyToClipboard(this.parentNode)" viewBox="0 0 64 64">
<title>Copy</title>
<rect x="16" y="16" width="48" height="48" rx="6" />
<path d="M.5,47.52V6.5a6,6,0,0,1,6-6h41" />
</svg>
</div>
<sm-button id="sign_out_btn" onclick="signOut()">Sign out</sm-button>
</section>
<section>
<h4>Theme</h4>
<div class="flex align-center space-between">
<p>Turn dark theme on/off.</p>
<sm-switch id="theme_switcher"></sm-switch>
</div>
</section>
</sm-popup>
<!-- Add a person popup -->
<sm-popup id="add_person_popup">
<header class="popup-header" slot="header">
<svg class="icon" onclick="this.closest('sm-popup').hide()" viewBox="0 0 64 64">
<title>close popup</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
<h4>
Add new person
</h4>
<button onclick="addPerson()" class="primary-btn expand" type="submit" disabled>
Add
</button>
</header>
<sm-input id="person_flo_id_input" floId placeholder="FLO ID*" animate required></sm-input>
<sm-input id="person_name_input" placeholder="Name*" animate required></sm-input>
<div id="specify_details" class="grid">
<div class="flex direction-column">
<h4>Additional details</h4>
<p>Add more details about a person. Specify type of information and actual detail.</p>
</div>
<div id="additional_fields" class="grid"></div>
<div id="add_detail" class="grid align-center">
<sm-input id="add_detail_type_input" exclude placeholder="Type"></sm-input>
<sm-input id="add_detail_value_input" exclude placeholder="Value"></sm-input>
<svg class="icon" onclick="addDetails()" viewBox="0 0 64 64">
<title>Add detail</title>
<polyline points="0.35 31.82 21.45 52.98 63.65 10.66" />
</svg>
</div>
</div>
</sm-popup>
<!-- Create new sheet popup -->
<sm-popup id="new_sheet_popup">
<header class="popup-header" slot="header">
<svg class="icon" onclick="this.closest('sm-popup').hide()" viewBox="0 0 64 64">
<title>close popup</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
<h4>
Create new sheet
</h4>
<button onclick="createSheet()" class="primary-btn expand" type="submit" disabled>
Create
</button>
</header>
<sm-input id="sheet_title_input" placeholder="Name*" animate required></sm-input>
<sm-textarea id="sheet_description_input" placeholder="Description" animate required></sm-textarea>
<div id="specify_columns" class="grid">
<div class="flex direction-column">
<h4>Add Columns</h4>
<p>Columns will be added as the order you added them.</p>
</div>
<div id="columns_container" class="flex"></div>
<div id="add_column" class="flex align-center space-between">
<sm-input id="add_column_input" exclude placeholder="Column title"></sm-input>
<svg class="icon" onclick="addColumn()" viewBox="0 0 64 64">
<title>Add this column</title>
<polyline points="0.35 31.82 21.45 52.98 63.65 10.66" />
</svg>
</div>
</div>
<div class="flex align-center space-between top-margin">
Make sheet private
<sm-switch id="sheet_type_switch" onchange="toggleEditors()"></sm-switch>
</div>
<div id="specify_editors" class="grid hide-completely">
<div class="flex direction-column">
<h4>Add editors</h4>
<p>Only specified editors will be able to update this sheet.</p>
</div>
<div id="editors_container" class="grid"></div>
<div id="add_editor" class="flex align-center space-between">
<sm-input id="add_editor_input" exclude placeholder="Editor's FLO Address"></sm-input>
<svg class="icon" onclick="addEditor()" viewBox="0 0 64 64">
<title>Add this editor</title>
<polyline points="0.35 31.82 21.45 52.98 63.65 10.66" />
</svg>
</div>
</div>
</sm-popup>
<!-- Group by popup -->
<sm-popup id="group_by">
<header class="popup-header" slot="header">
<svg class="icon" onclick="this.closest('sm-popup').hide()" viewBox="0 0 64 64">
<title>close popup</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
<h4 id="group_by_title">
</h4>
</header>
<div class="flex align-center" id="group_by_menu">
<sm-select id="first_select" class="justify-right">
<sm-option value="count">Count</sm-option>
<sm-option value="total" selected>Total</sm-option>
<sm-option value="avg">Avg</sm-option>
<sm-option value="max">Max</sm-option>
<sm-option value="min">Min</sm-option>
</sm-select>
<sm-select id="second_select"></sm-select>
</div>
<div class="table-container" id="group_by_view"></div>
</sm-popup>
<div id="save_button" class="hide flex align-center space-between" data-unsaved="0">
<span id="changes_indicator"></span>
<span>Changes needs to be saved!</span>
<sm-button>Save</sm-button>
</div>
<main>
<div id="sign_in_page" class="page grid space-between">
<div class="info">
<h4>RanchiMall</h4>
<h1>LogSheet</h1>
<p>Open &bull; Distributed &bull; Reliable</p>
</div>
<div class="sign-in-box flex direction-column">
<sm-tab-header target="user_entry">
<sm-tab>Sign In</sm-tab>
<sm-tab>Sign Up</sm-tab>
</sm-tab-header>
<sm-tab-panels id="user_entry">
<sm-panel>
<h3>Welcome back</h3>
<p>Just enter your FLO private key to continue.</p>
<form action="" onsubmit="return false">
<sm-input id="get_priv_key_field" privateKey placeholder="FLO Private Key" type="password"
required animate>
</sm-input>
<button id="sign_in_btn" class="primary-btn expand" type="submit" disabled>
Sign In
</button>
</form>
</sm-panel>
<sm-panel>
<h3>Get started</h3>
<p>Create your FLO public and private key pair. <Strong>Don't forget to store them
securely!</Strong></p>
<sm-button id="generate_flo_id" onclick="generateId()" variant="primary">Get FLO credentials
</sm-button>
<section id="credentials_section" class="hide-completely">
<h5>FLO ID</h5>
<div class="copy-row">
<h4 id="generated_id" class="copy"></h4>
<svg class="icon" onclick="copyToClipboard(this.parentNode)" viewBox="0 0 64 64">
<title>Copy</title>
<rect x="16" y="16" width="48" height="48" rx="6" />
<path d="M.5,47.52V6.5a6,6,0,0,1,6-6h41" />
</svg>
</div>
<h5>Private key</h5>
<div class="copy-row">
<h4 id="generated_key" class="copy"></h4>
<svg class="icon" onclick="copyToClipboard(this.parentNode)" viewBox="0 0 64 64">
<title>Copy</title>
<rect x="16" y="16" width="48" height="48" rx="6" />
<path d="M.5,47.52V6.5a6,6,0,0,1,6-6h41" />
</svg>
</div>
</section>
</sm-panel>
</sm-tab-panels>
</div>
</div>
<header id="main_header" class="grid">
<div class="flex align-center space-between">
<div class="flex direction-column">
<h5>RanchiMall</h5>
<h4>FLO LogSheet</h4>
</div>
<svg id="user_icon" onclick="showPopup('user_popup')" class="icon" viewBox="0 0 64 64">
<title>user</title>
<path d="M31.6,0.5c8.3,0,14.2,5.6,14.2,15S41.1,32.9,32,32.9s-13.8-8-13.8-17.3s5.9-15,14.2-15" />
<path d="M25.4,33.4c-2.5,4.7-10.5,7.1-16.4,9.7c-2.4,1-4.4,14-1.6,14c7.7,4.4,16.4,6.6,25.2,6.4
c8.5,0.3,16.9-2,24.2-6.4c2.8,0,0.8-12.9-1.6-14c-5.9-2.6-13.9-5-16.4-9.7" />
</svg>
</div>
</header>
<section id="home_page" class="page grid hide-completely">
<section id="main_section">
<header class="flex align-center space-between section-header">
<h4>Sheets</h4>
<sm-input id="search_sheets" placeholder="Search sheets" type="search"></sm-input>
</header>
<section id="sheets_container" class="grid"></section>
</section>
</section>
<section id="sheet_page" class="page toggle-side-bar grid hide-completely">
<nav id="side_bar">
<div class="flex align-center">
<svg class="icon hide-on-desktop" onclick="toggleSideBar()" viewBox="0 0 64 64">
<title>Go back to sheet</title>
<polyline points="48.01 0.35 16.35 32 48.01 63.65" />
</svg>
<h4 class="section-header">People</h4>
<sm-button class="small sub-admin-option justify-right" onclick="showPopup('add_person_popup')">Add
person</sm-button>
</div>
<div id="people_container" class="grid"></div>
</nav>
<section id="right">
<section id="sheet_details" class="collapse">
<div class="flex align-center">
<svg class="icon" onclick="showPage('home_page')" id="go_to_home" viewBox="0 0 64 64">
<title>Go back to homepage</title>
<polyline points="48.01 0.35 16.35 32 48.01 63.65" />
</svg>
<h5>Sheets</h5>
<sm-button class="small justify-right" onclick="showPopup('group_by')">Group by</sm-button>
</div>
<div class="flex align-center">
<svg class="icon" onclick="toggleSideBar()" viewBox="0 0 64 64">
<title>Reveal/hide side bar</title>
<line y1="18.5" x2="64" y2="18.5" />
<line y1="45.5" x2="42" y2="45.5" />
</svg>
<h3 id="sheet_heading"></h3>
<h5 id="sheet_type"></h5>
<svg id="toggle_details" class="icon justify-right" onclick="toggleDetails()"
viewBox="0 0 64 64">
<title>expand/collapse</title>
<polyline points="63.65 15.99 32 47.66 0.35 15.99" />
</svg>
</div>
<div id="sheet_editors" class="flex align-center"></div>
<p id="sheet_description"></p>
</section>
<div id="sheet_container">
<table>
<thead>
<tr></tr>
</thead>
<tbody></tbody>
</table>
</div>
<form id="new-log" style="display: none;" onsubmit="enterLog(event); return false;"></form>
<button type="submit" form="new-log" style="display: none;"></button>
</section>
</section>
</main>
<script src="components.js"></script>
<script>
let frag = document.createDocumentFragment(),
notificationSound = document.getElementById('notification_sound');
//Checks for internet connection status
if (!navigator.onLine)
notify('There seems to be a problem connecting to the internet.', 'error', 'success', true)
window.addEventListener('offline', () => {
notify('There seems to be a problem connecting to the internet.', 'error', true, true)
})
window.addEventListener('online', () => {
notifications.clearAll()
notify('We are back online.', '', '', true)
})
let themeSwitcher = document.getElementById('theme_switcher')
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 required for popups or modals to appear
class Stack {
constructor() {
this.items = [];
}
push(element) {
this.items.push(element);
}
pop() {
if (this.items.length == 0)
return "Underflow";
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
}
let popupStack = new Stack(),
zIndex = 10;
function showPopup(popup, pinned) {
let thisPopup = document.getElementById(popup);
zIndex++
thisPopup.setAttribute('style', `z-index: ${zIndex}`)
popupStack = thisPopup.show(pinned, popupStack)
return thisPopup;
}
// hides the popup or modal
function hidePopup() {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide()
}
document.addEventListener('popupclosed', e => {
let popup = e.detail.popup
switch (popup.id) {
case 'new_sheet_popup':
columnsContainer.innerHTML = ''
editorsContainer.innerHTML = ''
break;
}
})
const loaderPage = document.getElementById('main_loader'),
mainHeader = document.getElementById('main_header')
function showLoader() {
document.body.classList.remove('hide-completely')
loaderPage.classList.remove('hide-completely')
loader.classList.add('animate-loader')
document.querySelector('main').classList.add('hide-completely')
mainHeader.classList.add('hide-completely')
}
function hideLoader() {
loaderPage.classList.add('hide-completely')
loader.classList.remove('animate-loader')
document.querySelector('main').classList.remove('hide-completely')
mainHeader.classList.remove('hide-completely')
}
function setAttributes(el, attrs) {
for (var key in attrs) {
el.setAttribute(key, attrs[key]);
}
}
function randomHsl(saturation = 80, lightness = 80) {
let hue = Math.random() * 360
let color = {
primary: `hsla( ${hue}, ${saturation}%, ${lightness}%, 1)`,
light: `hsla( ${hue}, ${saturation}%, 90%, 0.6)`
}
return color;
}
let notifications = document.getElementById('show_message');
//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 if (mode === 'warn')
console.warn(message)
else
console.log(message)
notifications.push(message, mode, pinned)
if (navigator.onLine && sound) {
notificationSound.currentTime = 0;
notificationSound.play();
}
}
// displays a popup for asking permission. Use this instead of JS confirm
let getConfirmation = (message, cancelText = 'Cancel', confirmText = 'OK') => {
return new Promise(resolve => {
let popup = document.getElementById('confirmation');
showPopup('confirmation')
popup.querySelector('#confirm_message').textContent = message;
let submitButton = popup.querySelector('.submit-btn')
submitButton.textContent = confirmText
submitButton.onclick = () => {
popup.hide()
resolve(true);
}
let cancelButton = popup.querySelector('.cancel-btn')
cancelButton.textContent = cancelText
cancelButton.onclick = () => {
popup.hide()
reject(false);
}
})
}
// displays a popup for asking user input. Use this instead of JS prompt
let askPrompt = function (message, defaultVal) {
return new Promise(resolve => {
let popup = document.getElementById('prompt'),
input = popup.querySelector('sm-input');
if (defaultVal)
input.value = defaultVal;
showPopup('prompt')
input.focus()
input.addEventListener('keyup', e => {
if (e.key === 'Enter') {
resolve(input.value);
popup.hide()
}
})
popup.querySelector('#prompt_message').textContent = message;
popup.querySelector('.submit-btn').onclick = () => {
popup.hide()
resolve(input.value);
}
popup.querySelector('.cancel-btn').onclick = () => {
popup.hide()
resolve(null);
}
})
}
const currentYear = new Date().getFullYear()
function formatedTime(time, relative) {
try {
if (String(time).indexOf('_'))
time = String(time).split('_')[0]
let timeFrag = new Date(parseInt(time)).toString().split(' '),
day = timeFrag[0],
month = timeFrag[1],
date = timeFrag[2],
year = timeFrag[3],
minutes = new Date(parseInt(time)).getMinutes(),
hours = new Date(parseInt(time)).getHours(),
currentTime = new Date().toString().split(' ')
minutes = minutes < 10 ? `0${minutes}` : minutes
let finalHours = ``;
if (hours > 12)
finalHours = `${hours - 12}:${minutes}`
else if (hours === 0)
finalHours = `12:${minutes}`
else
finalHours = `${hours}:${minutes}`
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`
if (relative) {
if (year == currentYear) {
if (currentTime[1] === month && date === currentTime[2])
return `Today at ${finalHours}`;
else
return ` ${date} ${month}`;
}
}
else
return `${finalHours} ${month} ${date} ${year}`;
} catch (e) {
console.error(e);
return time;
}
}
function copyToClipboard(parent) {
let toast = document.getElementById('textCopied'),
textToCopy = parent.querySelector('.copy').textContent;
navigator.clipboard.writeText(textToCopy)
notify('Copied', 'success')
}
function areInputsValid(parent) {
let allInputs = parent.querySelectorAll("sm-input:not([disabled])"),
inputsFilled = [...allInputs].every(input => {
if (input.hasAttribute('floId')) {
if (floCrypto.validateAddr(input.value.trim())) return true
}
else if (input.hasAttribute('privateKey')) {
if (floCrypto.getPubKeyHex(input.value.trim())) return true
}
else
return input.isValid
})
let allRadios = parent.querySelectorAll("input[type='radio']")
if (allRadios.length)
return inputsFilled && parent.querySelector('[checked]')
else
return inputsFilled
}
function formValidation(formElement) {
let parent = formElement.closest('sm-popup') || formElement.closest('form'),
submitBtn = parent.querySelector("[type='submit']");
if (!submitBtn) return;
if (areInputsValid(parent))
submitBtn.disabled = false;
else
submitBtn.disabled = true;
}
window.addEventListener('load', () => {
document.addEventListener('input', e => {
if (e.target.closest('sm-input')) {
let input = e.target.closest('sm-input')
if (!input.closest('sm-popup') && !input.closest('form')) return
formValidation(input)
if (input.value === '')
input.setValidity('')
let validityState = input.validity
if (input.hasAttribute('type') && input.getAttribute('type') === 'number') {
if (validityState.rangeUnderflow) {
input.setValidity('Minium ₹1 should be entered.')
}
else if (validityState.typeMismatch) {
input.setValidity('Only digits are allowed.')
}
else {
input.setValidity('')
}
}
else if (input.hasAttribute('floId')) {
if (floCrypto.validateAddr(input.value.trim()) || input.value.trim() === '')
input.setValidity('')
else
input.setValidity('Invalid FLO address.')
}
else if (input.hasAttribute('privateKey')) {
if (floCrypto.getPubKeyHex(input.value.trim()) || input.value.trim() === '')
input.setValidity('')
else
input.setValidity('Invalid FLO private key!')
}
else {
if (validityState.patternMismatch)
input.setValidity('Invalid UPI address.')
if (validityState.tooShort || validityState.tooLong)
input.setValidity('UPI transaction Id should be 12 digits long.')
else
input.setValidity('')
}
}
})
document.addEventListener('keyup', (e) => {
if (e.target.closest('sm-input')) {
if (e.key === 'Enter') {
let parent = e.target.closest('sm-popup') || e.target.closest('form')
parent.querySelector("button[type='submit']")
.click();
}
}
})
let currentTableHeader
document.addEventListener('click', e => {
try {
if (e.target.closest('#add_new_sheet'))
showPopup('new_sheet_popup')
else if (e.target.closest('.sheet-card'))
viewSheet(e.target.closest('.sheet-card').dataset.name)
else if (e.target.closest(".person-card"))
copyToLogInput(e.target.closest(".person-card").dataset.floId)
else if (e.target.closest("th")) {
let trgt = e.target.closest('th')
if (currentTableHeader && currentTableHeader !== trgt)
currentTableHeader.classList.remove('ascending', 'descending')
sortTable(trgt.cellIndex, trgt.classList.contains('descending'))
if (trgt.classList.contains('descending'))
trgt.classList.add('ascending')
else
trgt.classList.remove('ascending')
trgt.classList.toggle('descending')
currentTableHeader = trgt
}
else if (e.target.closest('h4')) {
createRipple(e)
}
} catch (error) {
//do nothing
}
})
function createRipple(event) {
const target = event.target.closest('h4')
const ripple = target.querySelector('.ripple');
if (ripple) {
ripple.remove();
}
const ogOverflow = target.style.overflow
target.style.overflow = 'hidden'
console.log(target.getBoundingClientRect(), event)
const circle = document.createElement("span");
const diameter = Math.max(target.clientWidth, target.clientHeight);
const radius = diameter / 2;
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - radius}px`;
circle.style.top = `${event.clientY - radius}px`;
circle.classList.add("ripple");
target.append(circle);
circle.onanimationend = () => {
target.style.overflow = ogOverflow
}
}
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
let { title, description, editors, attributes, sheet, isWriteable, isSubAdmin } = floGlobals.currentSheet
renderSheetView(title, description, editors, attributes, sheet, isWriteable, isSubAdmin, true, true)
observer.disconnect()
}
})
})
const watchLastElement = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
observer.observe(mutation.addedNodes[mutation.addedNodes.length - 1])
}
})
})
watchLastElement.observe(tableBody, { childList: true, subtree: true })
document.getElementById('search_sheets').addEventListener('input', function () {
if (this.value.trim !== '') {
for (child of sheetsContainer.children) {
if (child.id === 'add_new_sheet') continue
if (child.dataset.name.toUpperCase().indexOf(this.value.toUpperCase()) > -1) {
child.classList.remove('hide-completely')
}
else {
child.classList.add('hide-completely')
}
}
}
})
})
function removeUnderscore(word) {
return word.replace(/_/g, ' ')
}
const render = {
sheetCard(sheetName, editors) {
let card = document.createElement('div'),
type = 'Public'
card.classList.add('sheet-card')
card.setAttribute('data-name', sheetName)
if (editors)
type = 'Private'
card.innerHTML = `
<div class="card">
<h5>${type}</h5>
</div>
<h4>${removeUnderscore(sheetName)}</h4>
`
return card
},
personCard(floId, details) {
let card = document.createElement('div'),
{ name } = details
card.classList.add('person-card')
setAttributes(card, {
'data-flo-id': floId,
'data-name': name
})
card.innerHTML = `
<h3 class="person-initials">${name.charAt(0)}</h3>
<h4 class="person-name">${name}</h4>
<h5 class="person-flo-id overflow-ellipsis">${floId}</h5>
`
return card
},
editorCard(floId) {
let card = document.createElement('div')
card.classList.add('editor-card', 'flex', 'space-between', 'align-center')
setAttributes(card, {
'data-flo-id': floId
})
card.innerHTML = `
<h4 class="editor-address">${floId}</h4>
<svg class="icon" onclick="this.parentNode.remove()" viewBox="0 0 64 64">
<title>Remove editor</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
`
return card
},
columnCard(title) {
let card = document.createElement('div')
card.classList.add('column-card', 'flex', 'space-between', 'align-center')
setAttributes(card, {
'data-column-title': title
})
card.innerHTML = `
<h5 class="column-title">${title}</h5>
<svg class="icon" onclick="this.parentNode.remove()" viewBox="0 0 64 64">
<title>Remove column</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
`
return card
},
detailsCard(type, value) {
let card = document.createElement('div')
card.classList.add('details-card', 'grid', 'align-center')
setAttributes(card, {
'data-type': type,
'data-value': value
})
card.innerHTML = `
<h5 class="type">${type}</h5>
<h4 class="value">${value}</h4>
<svg class="icon" onclick="this.parentNode.remove()" viewBox="0 0 64 64">
<title>Remove this field</title>
<line x1="64" y1="0" x2="0" y2="64" />
<line x1="64" y1="64" x2="0" y2="0" />
</svg>
`
return card
}
}
const sheetDetails = document.getElementById('sheet_details')
const sheetPage = document.getElementById('sheet_page')
const addEditorInput = document.getElementById('add_editor_input')
const specifyEditors = document.getElementById('specify_editors')
const editorsContainer = document.getElementById('editors_container')
const addColumnInput = document.getElementById('add_column_input')
const columnsContainer = document.getElementById('columns_container')
const additionalFields = document.getElementById('additional_fields')
const personFloIdInput = document.getElementById('person_flo_id_input')
const personNameInput = document.getElementById('person_name_input')
const addDetailTypeInput = document.getElementById('add_detail_type_input')
const addDetailValueInput = document.getElementById('add_detail_value_input')
function addEditor() {
let address = addEditorInput.value.trim()
if (address === '') return;
if (floCrypto.validateAddr(address)) {
editorsContainer.append(render.editorCard(address))
addEditorInput.value = ''
}
else {
notify('Invalid editor FLO address', 'error')
}
}
function addColumn() {
let columnTitle = addColumnInput.value.trim()
if (columnTitle !== '') {
columnsContainer.append(render.columnCard(columnTitle))
addColumnInput.value = ''
}
else
notify("Column name should be specified.", 'error')
}
function addDetails() {
let type = addDetailTypeInput.value.trim()
let value = addDetailValueInput.value.trim()
if (type !== '' && value !== '') {
additionalFields.append(render.detailsCard(type, value))
addDetailTypeInput.value = ''
addDetailValueInput.value = ''
}
else
notify('Please enter both type and value', 'error')
}
function toggleDetails() {
sheetDetails.classList.toggle('collapse')
}
function toggleSideBar() {
sheetPage.classList.toggle('toggle-side-bar')
}
function toggleEditors() {
specifyEditors.classList.toggle('hide-completely')
}
const allPages = document.querySelectorAll('.page')
function showPage(page) {
allPages.forEach(page => page.classList.add('hide-completely'))
document.getElementById(page).classList.remove('hide-completely')
if (page === 'home_page') {
tableBody.innerHTML = ''
sheetHeading.textContent = ''
sheetType.textContent = ''
sheetDescription.textContent = ''
mainHeader.classList.remove('hide-completely')
}
else
mainHeader.classList.add('hide-completely')
}
const credentialsSection = document.getElementById('credentials_section'),
generateFloId = document.getElementById('generate_flo_id'),
generatedId = document.getElementById('generated_id'),
generatedKey = document.getElementById('generated_key')
function generateId() {
generateFloId.classList.add('hide-completely')
credentialsSection.classList.remove('hide-completely')
let { floID, privKey } = floCrypto.generateNewID()
generatedId.textContent = floID
generatedKey.textContent = privKey
}
function signOut() {
getConfirmation('Are you sure you want to Sign out?').then(result => {
try {
hidePopup()
generateFloId.classList.remove('hide-completely')
credentialsSection.classList.add('hide-completely')
generatedId.textContent = ''
generatedKey.textContent = ''
floDapps.clearCredentials();
setTimeout(onLoadStartUp, 1000)
} catch (error) {
notify(error, "error")
}
})
}
document.getElementById('save_button').children[2].addEventListener("click", function (e) {
logSheet.commitUpdates().then(result => {
this.parentNode.classList.add('hide')
this.parentNode.classList.remove('no-transformations')
this.parentNode.dataset["unsaved"] = '0'
notify("Save Successful")
}).catch(error => notify(`Save Unsuccessful [${error}]`, error))
})
const firstSelect = document.getElementById('first_select')
const secondSelect = document.getElementById('second_select')
document.getElementById('first_select').addEventListener("change", function (e) {
try {
let title = floGlobals.currentSheet.title;
let sheet = floGlobals.currentSheet.sheet;
let type = this.value;
console.log(this)
if (type === 'count') {
secondSelect.classList.add("hide-completely")
let data = logSheet.groupLogsBy.count(title, sheet)
renderGroupByView("count", data)
} else {
secondSelect.classList.remove("hide-completely")
let column = secondSelect.value;
let data = logSheet.groupLogsBy[type](title, sheet, column);
renderGroupByView(`${type}(${column})`, data)
}
} catch (error) {
notify(error, "error")
}
})
document.getElementById('second_select').addEventListener("change", function (e) {
try {
console.log(this)
let title = floGlobals.currentSheet.title;
let sheet = floGlobals.currentSheet.sheet;
let type = firstSelect.value;
let column = this.value;
let data = logSheet.groupLogsBy[type](title, sheet, column);
renderGroupByView(`${type}(${column})`, data)
} catch (error) {
notify(error, "error")
}
})
function removeElement(element) {
element.parentNode.removeChild(element);
}
function clearElement(element) {
element.innerHTML = '';
return element;
}
let banner = document.getElementById('save_button')
function showSave() {
banner.dataset["unsaved"] = parseInt(banner.dataset["unsaved"]) + 1
banner.classList.remove('no-transformations');
banner.classList.add('hide');
setTimeout(() => {
banner.children[0].style.background = `#F${Math.max(9 - parseInt(banner.dataset["unsaved"]), 0)}0`;
banner.classList.add('no-transformations');
banner.classList.remove('hide');
}, 300)
}
function addPerson() {
try {
let floID = personFloIdInput.value.trim();
let name = personNameInput.value.trim();
let otherDetails = {}
let fields = additionalFields.querySelectorAll('.type, .value')
for (let i = 0; i < fields.length; i += 2)
otherDetails[fields[i].textContent] = fields[i + 1].textContent
console.log(fields, otherDetails)
logSheet.addPerson(floID, name, otherDetails)
peopleContainer.append(render.personCard(floID, logSheet.viewPerson(floID)))
hidePopup()
notify(`Added Person: ${floID}`)
showSave()
} catch (error) {
notify(error, "error")
}
}
const sheetTitleInput = document.getElementById('sheet_title_input'),
sheetDescriptionInput = document.getElementById('sheet_description_input'),
sheetTypeSwitch = document.getElementById('sheet_type_switch')
function createSheet() {
try {
let title = sheetTitleInput.value.trim().replace(/ /g, "_");
let description = sheetDescriptionInput.value.trim();
let columns = []
let editors = []
columnsContainer.querySelectorAll('.column-title').forEach(column => columns.push(column.textContent))
if (!columns.length) {
notify('Every LogSheet should contain atleast 1 column.', 'error')
return
}
editorsContainer.querySelectorAll('.editor-address').forEach(editor => editors.push(editor.textContent))
if (!sheetTypeSwitch.checked)
editors = null;
else
if (!editors.length)
editors = floGlobals.subAdmins;
logSheet.createNewSheet(title, description, columns, editors)
let sheet = {}
sheet[title] = { editors }
renderSheetList(sheet)
hidePopup()
notify(`Created New Sheet: ${title}`)
showSave()
} catch (error) {
notify(error, "error")
}
}
function copyToLogInput(floID) {
let input = document.forms['new-log'][0]
if (input.tagName === 'INPUT')
input.value = floID;
console.log(floID, input)
}
function enterLog(e) {
e.preventDefault()
let title = floGlobals.currentSheet.title;
let form = document.forms['new-log'],
allFormElements = document.querySelectorAll('input[form="new-log"]')
allFormElements.forEach(element => element.disabled = true)
if (form[0].tagName === 'INPUT') {
let floID = form[0].value;
let log = []
for (let i = 1; i < form.length - 1; i++)
log.push(form[i].value)
logSheet.enterLog(title, floID, log).then(result => {
allFormElements.forEach(element => element.disabled = false)
form.reset();
notify('Log entry successful', 'success')
let row = tableBody.insertRow(0);
row.insertCell().textContent = floID
row.insertCell().innerHTML = `<input type="text" class="grade-input" disabled>`
log.forEach(l => row.insertCell().textContent = l)
}).catch(error => {
notify(error, "error")
allFormElements.forEach(element => element.disabled = false)
})
}
}
function viewSheet(title) {
sheetContainer.classList.add('placeholder')
sheetHeading.classList.add('placeholder')
sheetType.classList.add('placeholder')
sheetDescription.classList.add('placeholder')
tableHeader.classList.add('hide')
showPage('sheet_page')
logSheet.refreshLogs(title).then(result => {
sheetContainer.classList.remove('placeholder')
sheetHeading.classList.remove('placeholder')
sheetType.classList.remove('placeholder')
sheetDescription.classList.remove('placeholder')
tableHeader.classList.remove('hide')
let data = logSheet.viewLogs(title)
renderSheetView(data.title, data.description, data.editors, data.attributes, data.sheet.reverse(),
!data.editors || data.editors.includes(myFloID), floGlobals.subAdmins.includes(myFloID))
}).catch(error => notify(error, "error"))
}
function renderPersonCard(floID, details) {
let newCard = document.createElement("div");
newCard.classList.add("person-card"); { }
let frontSide = document.createElement("div");
let initalTag = document.createElement("span");
initalTag.textContent = details.name.charAt(0)
frontSide.appendChild(initalTag);
let nameTag = document.createTextNode(details.name);
frontSide.appendChild(nameTag);
let backSide = document.createElement("div");
let idTag = document.createElement("span");
idTag.textContent = floID;
backSide.appendChild(idTag);
for (d in details)
if (d !== "name") {
let detailTag = document.createElement("li")
detailTag.textContent = `${d}: ${details[d]}`
backSide.appendChild(detailTag)
}
newCard.appendChild(frontSide);
newCard.appendChild(backSide);
document.getElementById("people_container").appendChild(newCard);
}
const peopleContainer = document.getElementById("people_container")
function renderPersonList(personList = {}) {
for (person in personList)
frag.append(render.personCard(person, personList[person]))
peopleContainer.append(frag)
}
const sheetsContainer = document.getElementById("sheets_container")
function renderSheetList(sheetList) {
console.log(sheetList)
for (sheet in sheetList) {
frag.append(render.sheetCard(sheet, sheetList[sheet].editors))
}
if (floGlobals.subAdmins.includes(myFloID) && !document.getElementById('add_new_sheet')) {
let firstCard = document.createElement('div')
firstCard.id = 'add_new_sheet'
firstCard.classList.add('sheet-card')
firstCard.innerHTML = `
<div class="card">
<svg class="icon" viewBox="0 0 64 64">
<title>add</title>
<line x1="32" x2="32" y2="64" />
<line x1="64" y1="32" y2="32" />
</svg>
</div>
<h4>Add new sheet</h4>
`
frag.prepend(firstCard)
}
sheetsContainer.append(frag)
}
function createTh(text) {
let cell = document.createElement('th')
cell.textContent = text
return cell
}
function createCell(text) {
let cell = document.createElement('td')
/*if(text.split(/,|:| /).length > 5)
cell.classList.add('text-field')*/
cell.textContent = text
return cell
}
const sheetContainer = document.getElementById('sheet_container'),
sheetHeading = document.getElementById('sheet_heading'),
sheetType = document.getElementById('sheet_type'),
sheetDescription = document.getElementById('sheet_description'),
sheetEditors = document.getElementById('sheet_editors'),
tableHeader = sheetContainer.firstElementChild.firstElementChild,
tableBody = sheetContainer.firstElementChild.children[1]
let startingIndex = 0,
endingIndex = 0
function renderSheetView(title, description, editors, attributes, sheet, isWriteable, isSubAdmin, onlyRenderTable = false, lazyLoad = false) {
if (!lazyLoad)
floGlobals.currentSheet = {
title,
description,
editors,
attributes,
sheet,
isWriteable,
isSubAdmin,
}
const parseVectorClock = (vc) => {
vc = vc.split('_')
let time = new Date(parseInt(vc[0])).toString().slice(4, 24);
let floID = vc[1]
return `by ${floID} (${time})`
}
const createGradeField = (vc, grade) => {
let gradeField = document.createElement("input")
gradeField.setAttribute("type", "text")
gradeField.className = "grade-input"
gradeField.value = grade
if (!isWriteable || !isSubAdmin || vc.split('_')[1] == myFloID)
gradeField.disabled = true;
else {
gradeField.addEventListener("keypress", (event) => {
if (event.keyCode == 13) {
event.preventDefault();
gradeField.disabled = true;
logSheet.forwardLog(title, vc, gradeField.value)
.then(result => notify("Graded Log"))
.catch(error => notify("Grading failed: " + error, "error"))
.finally(_ => gradeField.disabled = false)
}
})
}
return gradeField
}
if (lazyLoad) {
startingIndex = sheet.length > endingIndex ? endingIndex : sheet.length
endingIndex = sheet.length - endingIndex > 20 ? endingIndex + 20 : sheet.length
} else {
startingIndex = 0
endingIndex = sheet.length > 20 ? 20 : sheet.length
}
if (!onlyRenderTable) {
//Add Sheet Details
sheetHeading.textContent = removeUnderscore(title);
if (editors) {
sheetType.textContent = 'Private'
editors.forEach(editor => {
let card = document.createElement('div')
card.className = 'editor'
card.textContent = editor;
frag.append(card)
})
sheetEditors.innerHTML = 'Maintained by: '
sheetEditors.append(frag)
}
else {
sheetType.textContent = 'Public'
sheetEditors.innerHTML = ''
}
sheetDescription.textContent = description;
clearElement(tableHeader)
let row = tableHeader.insertRow();
row.append(createTh("FLO ID"), createTh("Grade"));
attributes.forEach(a => row.append(createTh(a)))
//Add input fields if writable
if (isWriteable && !lazyLoad) {
let row = tableHeader.insertRow();
row.insertCell().innerHTML = `<input form="new-log" class="log-input" type="text">`;
row.insertCell().textContent = ``;
attributes.forEach(a => row.insertCell().innerHTML = `<input form="new-log" class="log-input" type="text">`);
}
}
if (onlyRenderTable) {
if (!lazyLoad)
tableBody.innerHTML = ''
}
for (let i = startingIndex; i < endingIndex; i++) {
let data = sheet[i]
if (!data.log || !data.floID || !data.vc)
continue;
let row = document.createElement('tr')
row.setAttribute("title", parseVectorClock(data.vc))
row.append(createCell(data.floID))
let gradeF = document.createElement('td')
gradeF.append(createGradeField(data.vc, data.grade))
row.append(gradeF)
data.log.forEach(l => row.append(createCell(l)))
frag.append(row)
}
tableBody.append(frag)
if (!onlyRenderTable) {
//Add options for groupBy
document.getElementById('group_by_title').textContent = `Group ${removeUnderscore(title)} by`;
document.getElementById('group_by_view').innerHTML =
`<table><thead><tr><th>FLO ID</th><th><i>null</i></th></tr></thead></table>`
let colSelect = clearElement(document.getElementById('group_by_menu').children[1]);
attributes.forEach(a => {
let opt = document.createElement('sm-option');
opt.setAttribute('value', a);
opt.textContent = a;
colSelect.appendChild(opt)
})
}
}
function renderGroupByView(groupName, groupData) {
let table = document.createElement("table")
let head = table.createTHead().insertRow(0)
head.insertCell(0).textContent = "FLO ID";
head.insertCell(1).textContent = groupName;
let tbody = document.createElement("tbody")
for (floID in groupData) {
let row = tbody.insertRow(0);
row.insertCell(0).textContent = floID
row.insertCell(1).textContent = groupData[floID]
}
table.appendChild(tbody);
clearElement(document.getElementById("group_by_view")).appendChild(table);
}
function sortTable(n, ascending) {
console.log(n)
if (ascending) {
if (n === 0)
floGlobals.currentSheet.sheet.sort((a, b) => a.floID.toLowerCase().localeCompare(b.floID.toLowerCase()))
else if (n === 1)
floGlobals.currentSheet.sheet.sort((a, b) => {
if (a.grade === b.grade) {
return 0;
}
else {
return (a.grade < b.grade) ? -1 : 1;
}
})
else
floGlobals.currentSheet.sheet.sort((a, b) => {
if (isNaN(a.log[n - 2])) {
let x = (a.log[n - 2]) ? a.log[n - 2].toLowerCase() : a.log[n - 2]
let y = (b.log[n - 2]) ? b.log[n - 2].toLowerCase() : b.log[n - 2]
if (x === y) {
return 0;
}
else {
return (x < y) ? -1 : 1;
}
}
else
return a.log[n - 2] - b.log[n - 2]
})
} else {
if (n === 0)
floGlobals.currentSheet.sheet.sort((a, b) => b.floID.toLowerCase().localeCompare(a.floID.toLowerCase()))
else if (n === 1)
floGlobals.currentSheet.sheet.sort((a, b) => {
if (a.grade === b.grade) {
return 0;
}
else {
return (a.grade > b.grade) ? -1 : 1;
}
})
else
floGlobals.currentSheet.sheet.sort((a, b) => {
if (isNaN(a.log[n - 2])) {
let x = (a.log[n - 2]) ? a.log[n - 2].toLowerCase() : a.log[n - 2]
let y = (b.log[n - 2]) ? b.log[n - 2].toLowerCase() : b.log[n - 2]
if (x === y) {
return 0;
}
else {
return (x > y) ? -1 : 1;
}
}
else
return b.log[n - 2] - a.log[n - 2]
})
}
let { title, description, editors, attributes, sheet, isWriteable, isSubAdmin } = floGlobals.currentSheet
renderSheetView(title, description, editors, attributes, sheet, isWriteable, isSubAdmin, true, false)
}
</script>
</body>
</html>