const uiGlobals = {}
const { html, svg, render: renderElem } = uhtml;
uiGlobals.connectionErrorNotification = []
//Checks for internet connection status
if (!navigator.onLine)
uiGlobals.connectionErrorNotification.push(notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error'))
window.addEventListener('offline', () => {
uiGlobals.connectionErrorNotification.push(notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error'))
})
window.addEventListener('online', () => {
uiGlobals.connectionErrorNotification.forEach(notification => {
getRef('notification_drawer').remove(notification)
})
notify('We are back online.', 'success')
})
// Use instead of document.getElementById
function getRef(elementId) {
return document.getElementById(elementId);
}
// displays a popup for asking permission. Use this instead of JS confirm
/**
@param {string} title - Title of the popup
@param {object} options - Options for the popup
@param {string} options.message - Message to be displayed in the popup
@param {string} options.cancelText - Text for the cancel button
@param {string} options.confirmText - Text for the confirm button
@param {boolean} options.danger - If true, confirm button will be red
*/
const getConfirmation = (title, options = {}) => {
return new Promise(resolve => {
const { message = '', cancelText = 'Cancel', confirmText = 'OK', danger = false } = options
getRef('confirm_title').innerText = title;
getRef('confirm_message').innerText = message;
const cancelButton = getRef('confirmation_popup').querySelector('.cancel-button');
const confirmButton = getRef('confirmation_popup').querySelector('.confirm-button')
confirmButton.textContent = confirmText
cancelButton.textContent = cancelText
if (danger)
confirmButton.classList.add('button--danger')
else
confirmButton.classList.remove('button--danger')
const { opened, closed } = openPopup('confirmation_popup')
confirmButton.onclick = () => {
closePopup({ payload: true })
}
cancelButton.onclick = () => {
closePopup()
}
closed.then((payload) => {
confirmButton.onclick = null
cancelButton.onclick = null
if (payload)
resolve(true)
else
resolve(false)
})
})
}
// 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);
};
}
// adds a class to all elements in an array
function addClass(elements, className) {
elements.forEach((element) => {
document.querySelector(element).classList.add(className);
});
}
// removes a class from all elements in an array
function removeClass(elements, className) {
elements.forEach((element) => {
document.querySelector(element).classList.remove(className);
});
}
// return querySelectorAll elements as an array
function getAllElements(selector) {
return Array.from(document.querySelectorAll(selector));
}
let zIndex = 50
// function required for popups or modals to appear
function openPopup(popupId, pinned) {
if (popupStack.peek() === undefined) {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closePopup()
}
})
}
zIndex++
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
return getRef(popupId).show({ pinned })
}
// hides the popup or modal
function closePopup(options = {}) {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide(options)
}
document.addEventListener('popupopened', async e => {
//pushes popup as septate entry in history
history.pushState({ type: 'popup' }, null, null)
switch (e.target.id) {
case '':
break;
}
})
document.addEventListener('popupclosed', e => {
zIndex--;
switch (e.target.id) {
case 'task_popup':
delete getRef('task_popup').dataset.taskId;
break;
}
if (popupStack.peek() === undefined) {
// if there are no more popups, do something
document.removeEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closePopup()
}
})
}
})
window.addEventListener('popstate', e => {
if (!e.state) return
switch (e.state.type) {
case 'popup':
closePopup()
break;
}
})
//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 = ``
break;
case 'error':
icon = ``
options.pinned = true
break;
}
if (mode === 'error') {
console.error(message)
}
return getRef("notification_drawer").push(message, { icon, ...options });
}
function getFormattedTime(timestamp, format) {
try {
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;
default:
return `${month} ${date} ${year}, ${finalHours}`;
}
} catch (e) {
console.error(e);
return timestamp;
}
}
// detect browser version
function detectBrowser() {
let ua = navigator.userAgent,
tem,
M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return 'IE ' + (tem[1] || '');
}
if (M[1] === 'Chrome') {
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera');
}
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
return M.join(' ');
}
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(
[
{
opacity: 1,
transform: `scale(0)`
},
{
transform: "scale(4)",
opacity: 0,
},
],
{
duration: 600,
fill: "forwards",
easing: "ease-out",
}
);
target.append(circle);
rippleAnimation.onfinish = () => {
circle.remove();
};
}
class Router {
/**
* @constructor {object} options - options for the router
* @param {object} options.routes - routes for the router
* @param {object} options.state - initial state for the router
* @param {function} options.routingStart - function to be called before routing
* @param {function} options.routingEnd - function to be called after routing
*/
constructor(options = {}) {
const { routes = {}, state = {}, routingStart, routingEnd } = options
this.routes = routes
this.state = state
this.routingStart = routingStart
this.routingEnd = routingEnd
this.lastPage = null
window.addEventListener('hashchange', e => this.routeTo(window.location.hash))
}
/**
* @param {string} route - route to be added
* @param {function} callback - function to be called when route is matched
*/
addRoute(route, callback) {
this.routes[route] = callback
}
/**
* @param {string} route
*/
handleRouting = async (page) => {
if (this.routingStart) {
this.routingStart(this.state)
}
if (this.routes[page]) {
await this.routes[page](this.state)
this.lastPage = page
} else {
if (this.routes['404']) {
this.routes['404'](this.state);
} else {
console.error(`No route found for '${page}' and no '404' route is defined.`);
}
}
if (this.routingEnd) {
this.routingEnd(this.state)
}
}
async routeTo(destination) {
try {
let page
let wildcards = []
let params = {}
let [path, queryString] = destination.split('?');
if (path.includes('#'))
path = path.split('#')[1];
if (path.includes('/'))
[, page, ...wildcards] = path.split('/')
else
page = path
this.state = { page, wildcards, lastPage: this.lastPage, params }
if (queryString) {
params = new URLSearchParams(queryString)
this.state.params = Object.fromEntries(params)
}
if (document.startViewTransition) {
document.startViewTransition(async () => {
await this.handleRouting(page)
})
} else {
// Fallback for browsers that don't support View transition API:
await this.handleRouting(page)
}
} catch (e) {
console.error(e)
}
}
}
// class based lazy loading
class LazyLoader {
constructor(container, elementsToRender, renderFn, options = {}) {
const { batchSize = 10, freshRender, bottomFirst = false, domUpdated } = options
this.elementsToRender = elementsToRender
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
this.renderFn = renderFn
this.intersectionObserver
this.batchSize = batchSize
this.freshRender = freshRender
this.domUpdated = domUpdated
this.bottomFirst = bottomFirst
this.shouldLazyLoad = false
this.lastScrollTop = 0
this.lastScrollHeight = 0
this.lazyContainer = document.querySelector(container)
this.update = this.update.bind(this)
this.render = this.render.bind(this)
this.init = this.init.bind(this)
this.clear = this.clear.bind(this)
}
get elements() {
return this.arrayOfElements
}
init() {
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
observer.disconnect()
this.render({ lazyLoad: true })
}
})
})
this.mutationObserver = new MutationObserver(mutationList => {
mutationList.forEach(mutation => {
if (mutation.type === 'childList') {
if (mutation.addedNodes.length) {
if (this.bottomFirst) {
if (this.lazyContainer.firstElementChild)
this.intersectionObserver.observe(this.lazyContainer.firstElementChild)
} else {
if (this.lazyContainer.lastElementChild)
this.intersectionObserver.observe(this.lazyContainer.lastElementChild)
}
}
}
})
})
this.mutationObserver.observe(this.lazyContainer, {
childList: true,
})
this.render()
}
update(elementsToRender) {
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
}
render(options = {}) {
let { lazyLoad = false } = options
this.shouldLazyLoad = lazyLoad
const frag = document.createDocumentFragment();
if (lazyLoad) {
if (this.bottomFirst) {
this.updateEndIndex = this.updateStartIndex
this.updateStartIndex = this.updateEndIndex - this.batchSize
} else {
this.updateStartIndex = this.updateEndIndex
this.updateEndIndex = this.updateEndIndex + this.batchSize
}
} else {
this.intersectionObserver.disconnect()
if (this.bottomFirst) {
this.updateEndIndex = this.arrayOfElements.length
this.updateStartIndex = this.updateEndIndex - this.batchSize - 1
} else {
this.updateStartIndex = 0
this.updateEndIndex = this.batchSize
}
this.lazyContainer.innerHTML = ``;
}
this.lastScrollHeight = this.lazyContainer.scrollHeight
this.lastScrollTop = this.lazyContainer.scrollTop
this.arrayOfElements.slice(this.updateStartIndex, this.updateEndIndex).forEach((element, index) => {
frag.append(this.renderFn(element))
})
if (this.bottomFirst) {
this.lazyContainer.prepend(frag)
// scroll anchoring for reverse scrolling
this.lastScrollTop += this.lazyContainer.scrollHeight - this.lastScrollHeight
this.lazyContainer.scrollTo({ top: this.lastScrollTop })
this.lastScrollHeight = this.lazyContainer.scrollHeight
} else {
this.lazyContainer.append(frag)
}
if (!lazyLoad && this.bottomFirst) {
this.lazyContainer.scrollTop = this.lazyContainer.scrollHeight
}
// Callback to be called if elements are updated or rendered for first time
if (!lazyLoad && this.freshRender)
this.freshRender()
}
clear() {
this.intersectionObserver.disconnect()
this.mutationObserver.disconnect()
this.lazyContainer.innerHTML = ``;
}
reset() {
this.arrayOfElements = (typeof this.elementsToRender === 'function') ? this.elementsToRender() : this.elementsToRender || []
this.render()
}
}
function buttonLoader(id, show) {
const button = typeof id === 'string' ? document.getElementById(id) : id;
if (!button) return
if (!button.dataset.hasOwnProperty('wasDisabled'))
button.dataset.wasDisabled = button.disabled
const animOptions = {
duration: 200,
fill: 'forwards',
easing: 'ease'
}
if (show) {
button.disabled = true
button.parentNode.append(document.createElement('sm-spinner'))
button.animate([
{
clipPath: 'circle(100%)',
},
{
clipPath: 'circle(0)',
},
], animOptions)
} else {
button.disabled = button.dataset.wasDisabled === 'true';
button.animate([
{
clipPath: 'circle(0)',
},
{
clipPath: 'circle(100%)',
},
], animOptions).onfinish = (e) => {
button.removeAttribute('data-original-state')
}
const potentialTarget = button.parentNode.querySelector('sm-spinner')
if (potentialTarget) potentialTarget.remove();
}
}
let isMobileView = false
const mobileQuery = window.matchMedia('(max-width: 40rem)')
function handleMobileChange(e) {
isMobileView = e.matches
}
mobileQuery.addEventListener('change', handleMobileChange)
handleMobileChange(mobileQuery)
const slideInLeft = [
{
opacity: 0,
transform: 'translateX(1.5rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutLeft = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(-1.5rem)'
},
]
const slideInRight = [
{
opacity: 0,
transform: 'translateX(-1.5rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutRight = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(1.5rem)'
},
]
const slideInDown = [
{
opacity: 0,
transform: 'translateY(-1.5rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutUp = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(-1.5rem)'
},
]
window.smCompConfig = {
'sm-input': [
{
selector: '[data-flo-address]',
customValidation: (value) => {
if (!value) return { isValid: false, errorText: 'Please enter a FLO address' }
return {
isValid: floCrypto.validateFloID(value),
errorText: `Invalid FLO address.
It usually starts with "F"`
}
}
},
{
selector: '[data-btc-address]',
customValidation: (value) => {
if (!value) return { isValid: false, errorText: 'Please enter a BTC address' }
return {
isValid: btcOperator.validateAddress(value),
errorText: `Invalid address.
It usually starts with "1", "3" or "bc1"`
}
}
},
{
selector: '[data-private-key]',
customValidation: (value, inputElem) => {
if (!value) return { isValid: false, errorText: 'Please enter a private key' }
if (floCrypto.getPubKeyHex(value)) {
const forAddress = inputElem.dataset.forAddress
if (!forAddress) return { isValid: true }
return {
isValid: btcOperator.verifyKey(forAddress, value),
errorText: `This private key does not match the address ${forAddress}`
}
} else
return {
isValid: false,
errorText: `Invalid private key. Please check and try again.`
}
}
},
{
selector: '[type="email"]',
customValidation: (value, target) => {
if (value === '') {
return {
isValid: false,
errorText: 'Please enter an email address'
}
}
return {
isValid: /\S+@\S+\.\S+/.test(value),
errorText: `Invalid email address`
}
}
}, {
selector: '#profile__whatsapp_number',
customValidation: (value, target) => {
if (value.length < 10) return { isValid: false, errorText: 'Number must be at least 10 digits long' }
if (value.length > 13) return { isValid: false, errorText: 'Number must be at most 13 digits long' }
return { isValid: true }
}
}
]
}
async function saveProfile() {
const name = getRef('profile__name').value.trim();
const email = getRef('profile__email').value.trim();
const college = getRef('profile__college').value.trim();
const course = getRef('profile__course').value.trim();
const whatsappNumber = getRef('profile__whatsapp_number').value.trim();
const stringifiedData = JSON.stringify({ name, email, college, course, whatsappNumber });
if (stringifiedData === floDapps.user.decipher(floGlobals.userProfile)) return notify('No changes detected', 'error')
const confirmation = await getConfirmation('Save details', {
message: 'Are you sure you want to save these details?',
confirmText: 'Save'
})
if (!confirmation) return;
const encryptedData = floDapps.user.encipher(stringifiedData);
buttonLoader('profile__save', true)
floCloudAPI.sendGeneralData({ encryptedData }, 'userProfile')
.then(response => {
notify('Profile saved successfully', 'success');
floGlobals.userProfile = encryptedData;
})
.catch(e => {
notify('An error occurred while saving the profile', 'error')
console.error(e)
}).finally(() => {
buttonLoader('profile__save', false)
})
}
async function applyToTask(id) {
if (!floGlobals.isUserLoggedIn) {
location.hash = '#/sign_in'
return notify('You need to be logged in to apply to a task')
}
const confirmation = await getConfirmation('Apply to task', { message: 'Are you sure you want to apply to this task?' })
if (!confirmation) return
// apply to task
floCloudAPI.sendGeneralData({ taskID: id }, 'taskApplications')
.then(response => {
notify('You have successfully applied to the task', 'success')
floGlobals.applications.add(id)
render.availableTasks();
}).catch(e => {
notify('An error occurred while applying to the task', 'error')
})
}
function editTask(id) {
const task = floGlobals.appObjects.rmInterns.tasks.find(task => task.id === id);
if (!task) return notify('Task not found', 'error')
const { title, description, category, deadline } = task;
getRef('task_popup__title_input').value = title;
getRef('task_popup__description').value = description;
getRef('task_popup__category').value = category;
getRef('task_popup__deadline').value = deadline;
getRef('task_popup').dataset.taskId = id;
openPopup('task_popup')
}
async function saveTask() {
const confirmation = await getConfirmation('Save task', {
message: 'Are you sure you want to save this task?',
confirmText: 'Save'
})
if (!confirmation) return;
// save task
const id = getRef('task_popup').dataset.taskId || Math.random().toString(36).substr(2, 9);
const title = getRef('task_popup__title_input').value;
const description = getRef('task_popup__description').value;
const category = getRef('task_popup__category').value;
const deadline = getRef('task_popup__deadline').value;
const task = {
id,
title,
description,
category,
deadline,
status: 'open'
}
const foundTask = floGlobals.appObjects.rmInterns.tasks.find(task => task.id === id);
if (foundTask) {
let taskDetailsChanged = false;
// edit task only if something has changed
for (const key in task) {
if (task[key] !== foundTask[key]) {
taskDetailsChanged = true;
foundTask[key] = task[key];
}
}
if (!taskDetailsChanged)
return notify('Please update at least one detail to save the changes', 'error')
} else {
task.date = Date.now();
floGlobals.appObjects.rmInterns.tasks.unshift(task)
}
floCloudAPI.updateObjectData('rmInterns')
.then(response => {
notify('Task saved successfully', 'success')
render.availableTasks();
})
.catch(e => {
notify('An error occurred while saving the task', 'error')
console.error(e)
}).finally(() => {
closePopup()
})
}
async function deleteTask(id) {
const confirmation = await getConfirmation('Delete task', {
message: 'Are you sure you want to delete this task?',
confirmText: 'Delete',
danger: true
})
if (!confirmation) return;
console
const taskIndex = floGlobals.appObjects.rmInterns.tasks.findIndex(task => task.id === id);
if (taskIndex < 0) return notify('Task not found', 'error');
// in case of error, add the task back to the list
const [cloneOfTaskToBeDeleted] = floGlobals.appObjects.rmInterns.tasks.splice(taskIndex, 1);
floCloudAPI.updateObjectData('rmInterns')
.then(response => {
notify('Task deleted successfully', 'success')
})
.catch(e => {
notify('An error occurred while deleting the task', 'error');
// add the task back to the list
floGlobals.appObjects.rmInterns.tasks.splice(taskIndex, 0, cloneOfTaskToBeDeleted);
}).finally(() => {
closePopup()
render.availableTasks();
})
}
const render = {
task(details = {}) {
const { title, description, date, id, status, deadline, category } = details;
let actions = '';
if (floGlobals.isSubAdmin) {
actions = html`
${floGlobals.applications[id].size} applied
`
} else if (!floGlobals.isAdmin) {
const applied = floGlobals.applications.has(id)
actions = html`
`
}
return html`
${description}
No tasks available
`) const tasksList = floGlobals.appObjects.rmInterns.tasks.map(render.task); renderElem(getRef(target), html`${tasksList}`) } } // routing logic const router = new Router({ routingStart(state) { if ("scrollRestoration" in history) { history.scrollRestoration = "manual"; } window.scrollTo(0, 0); }, routingEnd(state) { const { page, lastPage } = state if (lastPage !== page) { closePopup() } } }) const header = (page) => { const isUserLoggedIn = page === 'loading' || floGlobals.isUserLoggedIn return html`Welcome back, glad to see you again
New here? get your FLO login credentials
Don't share with anyone. Once lost private key can't be recovered.
You can use these FLO credentials with other RanchiMall apps too.