const uiGlobals = {} const { html, svg, render: renderElem } = uhtml; const { signal, computed, effect } = preactSignalsCore; 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 'profile_popup': renderElem(getRef('profile_popup__content'), html`

BTC integrated with FLO

You can use your FLO private key to perform transactions on the BTC network within our app ecosystem. The private key is the same for both.

My FLO address
My Bitcoin address
`) break; } }) document.addEventListener('popupclosed', e => { zIndex--; switch (e.target.id) { case 'task_popup': delete getRef('task_popup').dataset.taskId; break; case 'send_update_popup': renderElem(getRef('send_update_popup__content'), html``) 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. /** * @param {string} message - Message to be displayed in the notification * @param {string} mode - Mode of the notification. Can be 'success' or 'error' or '' * @param {object} options - Options for the notification * @param {boolean} options.pinned - If true, notification will not be dismissed automatically */ function notify(message, mode, options = {}) { let icon switch (mode) { case 'success': icon = `` break; case 'error': icon = `` if (!options.hasOwnProperty('timeout')) 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 startWorkingOnATask(id) { if (!floGlobals.isUserLoggedIn) { notify('You need to be logged in to start working on a task') location.hash = '#/sign_in'; return } const confirmation = await getConfirmation('Start working on this task?', { message: `Other tasks won't be available until you finish this task, continue?`, confirmText: 'Start working' }) if (!confirmation) return // send working on task acknowledgement floCloudAPI.sendGeneralData({ taskID: id }, 'taskApplications') .then(response => { notify('You can now start working on the task', 'success') floGlobals.applications.add(id) render.availableTasks(); }).catch(e => { notify('An error occurred while sending the application', 'error') console.error(e) }) } async function initSendingUpdate(id) { const updateType = signal('progress'); let updateForm async function sendUpdate(id) { try { const consent = await getConfirmation('Send update', { message: 'Are you sure you want to send this update?', confirmText: 'Send' }) if (!consent) return; console.log(updateType.value) if (updateType.value === 'status') { const status = getRef('send_update_popup__status__selector').value; console.log(status) if (status === 'in-progress') { notify('Send status update only when the task is completed', 'error') return } } buttonLoader('send_update_popup__submit', true) const message = getRef('update_field').value.trim(); const update = { id, type: updateType.value, message, date: Date.now() } // send update await floCloudAPI.sendGeneralData(update, 'taskUpdates') notify('Update sent successfully', 'success') closePopup() } catch (e) { notify('An error occurred while sending the update', 'error') console.error(e) } } effect(() => { switch (updateType.value) { case 'progress': updateForm = html`

What's the progress?

` break; case 'issue': updateForm = html`

What's the issue?

` break; case 'status': updateForm = html`

What's the status?

In progress Completed

Any additional details?

` break; } renderElem(getRef('send_update_popup__content'), html` updateType.value = e.target.value}> Progress Issue Status ${updateForm}
`) }) openPopup('send_update_popup') } getRef('send_update_popup').addEventListener('option-clicked', e => { console.log(e) }) function editTask(id) { 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 title = getRef('task_popup__title_input').value; const description = getRef('task_popup__description').value; const category = getRef('task_popup__category').value; const id = getRef('task_popup').dataset.taskId || category + '_' + Math.random().toString(36).substring(2, 9); const deadline = getRef('task_popup__deadline').value; const task = { id, title, description, category, deadline, status: 'open' } const foundTask = floGlobals.appObjects[category].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[category].tasks.unshift(task); } buttonLoader('task_popup__submit', true) floCloudAPI.updateObjectData(category) .then(response => { notify('Task saved successfully', 'success') render.availableTasks(); }) .catch(e => { notify('An error occurred while saving the task', 'error') console.error(e) }).finally(() => { buttonLoader('task_popup__submit', false) 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; const [category] = id.split('_'); const taskIndex = floGlobals.appObjects[category].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[category].tasks.splice(taskIndex, 1); floCloudAPI.updateObjectData(category) .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[category].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.isUserLoggedIn) { if (floGlobals.isSubAdmin) { actions = html` ${floGlobals.applications[id]?.size || 0} working ` } else if (!floGlobals.isAdmin) { // check if user has already working on the task const isThisActive = floGlobals.applications.has(id); const hasAnyActiveTask = isThisActive || [...floGlobals.applications].some(id => { const [category] = id.split('_'); return floGlobals.appObjects[category].tasks.some(task => floGlobals.applications.has(task.id)) }); if (isThisActive) { actions = html` ` } else if (hasAnyActiveTask) { actions = html`` } else { actions = html` ` } } } else { actions = html`Start working` floGlobals.applyingForTask = id; } return html`
  • ${title}

    ${description}

    ${actions}
  • ` }, availableTasks(options = {}) { const { type } = options; let availableTasks = [...floGlobals.interestedCategories || Object.keys(floGlobals.taskCategories)].flatMap(category => floGlobals.appObjects[category].tasks ) if (availableTasks.length === 0) return renderElem(getRef('available_tasks_list'), html`

    No tasks available

    `); if (type) { if (type === 'active') availableTasks = availableTasks.filter(task => floGlobals.applications.has(task.id)) else if (type === 'other') availableTasks = availableTasks.filter(task => !floGlobals.applications.has(task.id)) } console.log(availableTasks) availableTasks = availableTasks.map(task => { console.log(render.task(task)) return render.task(task) }); renderElem(getRef('available_tasks_list'), html`${availableTasks}`) } } // 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() } } }) let userAddressTimeInterval; let isShowingFloID = false; const header = () => { const { page } = router.state const isUserLoggedIn = page === 'loading' || floGlobals.isUserLoggedIn; if (userAddressTimeInterval) clearInterval(userAddressTimeInterval); userAddressTimeInterval = setInterval(() => { if (!isShowingFloID) { renderElem(getRef('user_popup_button'), html`
    ${floGlobals.myFloID}
    `) isShowingFloID = true } else { renderElem(getRef('user_popup_button'), html`
    ${floGlobals.myBtcID}
    `) isShowingFloID = false } }, 1000 * 10); return html`
    ${!['landing', 'loading'].includes(page) ? html`
    RanchiMall Selects
    ` : ''}
    ${isUserLoggedIn ? page !== 'loading' ? html` `: '' : html`
    ${page !== 'sign_up' ? html`Get Started` : ''} ${page !== 'sign_in' ? html`Sign in` : ''}
    `}
    `; } router.addRoute('loading', (state) => { renderElem(getRef('app_body'), html`
    ${header()}

    Loading RanchiMall Selects

    `); }) router.addRoute('landing', async (state) => { try { const { page } = state; if (floGlobals.interestedCategories.size) { await Promise.all( Object.keys(floGlobals.taskCategories).map(category => floCloudAPI.requestObjectData(category)) ) } renderElem(getRef('app_body'), html`
    ${header()} ${floGlobals.interestedCategories.size === 0 ? html`

    Welcome to
    RanchiMall Selects

    Select the categories you are interested in
    and we will show you the tasks available in those categories

    ` : html`

    Internship @ RanchiMall

    Available

    `}
    `) if (floGlobals.interestedCategories.size > 0) render.availableTasks() } catch (err) { notify(err, 'error') } }) function toggleCategory(e, category) { if (e.target.checked) { floGlobals.interestedCategories.add(category) } else { floGlobals.interestedCategories.delete(category) } } function saveCategories() { if (floGlobals.interestedCategories.size === 0) return notify('Please select at least one category', 'error', { timeout: 5000 }) localStorage.setItem('interestedCategories', JSON.stringify([...floGlobals.interestedCategories])); router.routeTo('landing'); } function handleSignIn() { privKeyResolver(getRef('private_key_field').value.trim()); router.routeTo('loading'); } router.addRoute('sign_in', (state) => { const { } = state; let dataset = {} if (!floGlobals.isPrivKeySecured) dataset.privateKey = '' renderElem(getRef('app_body'), html`
    ${header()}

    Sign in

    Welcome back, glad to see you again

    New here? get your FLO login credentials

    `); getRef('private_key_field').focusIn(); }) function handleSignUp() { const privKey = getRef('generated_private_key').value.trim(); privKeyResolver(privKey); router.routeTo('loading'); } router.addRoute('sign_up', (state) => { const { floID, privKey } = floCrypto.generateNewID() renderElem(getRef('app_body'), html`
    ${header()}

    Keep your keys safe!

    Don't share with anyone. Once lost private key can't be recovered.

    FLO address
    Private key

    You can use these FLO credentials with other RanchiMall apps too.

    `); }) function handleSubAdminViewChange(e) { location.hash = `#/home?view=${e.target.value}` } router.addRoute('', renderHome) router.addRoute('home', renderHome) function renderHome(state) { if (!floGlobals.isUserLoggedIn) { router.routeTo('landing'); return; } if (floGlobals.isAdmin) { } else if (floGlobals.isSubAdmin) { const { } = state; renderElem(getRef('app_body'), html`
    ${header()}
    Tasks
    `) getRef('task_popup__title').textContent = 'Add Task'; render.availableTasks() } else { const { params: { view = floGlobals.applications?.size ? 'active' : 'other' } } = state; if (floGlobals.applyingForTask) { startWorkingOnATask(floGlobals.applyingForTask) floGlobals.applyingForTask = null; } else { renderElem(getRef('app_body'), html`
    ${header()}

    Home

    ${floGlobals.applications?.size > 0 ? html` Active Other ` : html`

    Available

    `}
    `) render.availableTasks({ type: view }) } } } function handleViewChange(e) { location.hash = `#/home?view=${e.target.value}` } router.addRoute('task', (state) => { const { params: { id } } = state; if (floGlobals.isSubAdmin) { renderElem(getRef('app_body'), html`
    ${header()}
    Back

    Applications

    `) const applications = [...floGlobals.applications[id]].map(address => { return html`
  • ${address}
  • ` }) renderElem(getRef('task_applications_list'), html`${applications}`) } else if (!floGlobals.isAdmin) { } }) router.addRoute('profile', (state) => { const { } = state; let name = email = college = course = whatsappNumber = ''; if (floGlobals.userProfile) { const userDetails = JSON.parse(floDapps.user.decipher(floGlobals.userProfile)); name = userDetails.name; email = userDetails.email; college = userDetails.college; course = userDetails.course; whatsappNumber = userDetails.whatsappNumber; } renderElem(getRef('app_body'), html`
    ${header()}

    Tell us about

    yourself

    `) }) router.addRoute('404', async () => { renderElem(getRef('app_body'), html`

    404

    Page not found

    `); }) let privKeyResolver = null function getSignedIn(passwordType) { return new Promise((resolve, reject) => { privKeyResolver = resolve try { getPromptInput('Enter password', '', { isPassword: true, }).then(password => { if (password) { resolve(password) } }) } catch (err) { floGlobals.isPrivKeySecured = passwordType === 'PIN/Password'; if (!['#/landing', '#/sign_in', '#/sign_up'].some(route => window.location.hash.includes(route))) { history.replaceState(null, null, '#/landing') router.routeTo('#/landing') } } }); } function setSecurePassword() { if (!floGlobals.isPrivKeySecured) { const password = getRef('secure_pwd_input').value.trim(); floDapps.securePrivKey(password).then(() => { floGlobals.isPrivKeySecured = true; notify('Password set successfully', 'success'); closePopup(); }).catch(err => { notify(err, 'error'); }) } } function signOut() { getConfirmation('Sign out?', { message: 'You are about to sign out of the app, continue?', confirmText: 'Leave', cancelText: 'Stay' }) .then(async (res) => { if (res) { await floDapps.clearCredentials(); location.reload(); } }); } const btcAddresses = {} const floAddresses = {} function getBtcAddress(floAddress) { if (!btcAddresses[floAddress]) btcAddresses[floAddress] = btcOperator.convert.legacy2bech(floAddress) return btcAddresses[floAddress] } function getFloAddress(btcAddress) { if (!floAddresses[btcAddress]) floAddresses[btcAddress] = floCrypto.toFloID(btcAddress) return floAddresses[btcAddress] } router.routeTo('loading') window.addEventListener("load", () => { const [browserName, browserVersion] = detectBrowser().split(' '); const supportedVersions = { Chrome: 85, Firefox: 75, Safari: 13, } if (browserName in supportedVersions) { if (parseInt(browserVersion) < supportedVersions[browserName]) { notify(`${browserName} ${browserVersion} is not fully supported, some features may not work properly. Please update to ${supportedVersions[browserName]} or higher.`, 'error') } } else { notify('Browser is not fully compatible, some features may not work. for best experience please use Chrome, Edge, Firefox or Safari', 'error') } document.body.classList.remove('hidden') document.addEventListener('keyup', (e) => { if (e.key === 'Escape') { closePopup() } }) document.addEventListener('copy', () => { notify('copied', 'success') }) document.addEventListener("pointerdown", (e) => { if (e.target.closest("button:not(:disabled), .interactive:not(:disabled)")) { createRipple(e, e.target.closest("button, .interactive")); } }); const interestedCategories = localStorage.getItem('interestedCategories') || '[]'; floGlobals.interestedCategories = new Set(JSON.parse(interestedCategories)); floDapps.setMidStartup(() => new Promise((resolve, reject) => { floCloudAPI.requestObjectData('rmInterns') .then(() => { if (['#/landing', '#/sign_in', '#/sign_up'].some(route => window.location.hash.includes(route))) { router.routeTo(window.location.hash); } resolve() }).catch(err => { console.error(err) reject() }) }) ) floDapps.setCustomPrivKeyInput(getSignedIn) floDapps.launchStartUp().then(async result => { console.log(result) floGlobals.isUserLoggedIn = true floGlobals.myFloID = getFloAddress(floDapps.user.id); floGlobals.myBtcID = getBtcAddress(floGlobals.myFloID) floGlobals.isSubAdmin = floGlobals.subAdmins.includes(floGlobals.myFloID) floGlobals.isAdmin = floGlobals.myFloID === floGlobals.adminID let showingFloID = true try { if (floGlobals.isSubAdmin) { const promises = [] await Promise.all(Object.keys(floGlobals.taskCategories).map(category => floCloudAPI.requestObjectData(category))) for (const category in floGlobals.taskCategories) { if (!floGlobals.appObjects[category]) { console.log('resetting', category) floGlobals.appObjects[category] = { tasks: [], } promises.push(floCloudAPI.resetObjectData(category)) } } promises.push(floCloudAPI.requestGeneralData('taskApplications')) await Promise.all(promises) const taskApplications = floDapps.getNextGeneralData('taskApplications', '0'); floGlobals.applications = {} floGlobals.allAvailableTasks = new Set(); for (const category in floGlobals.taskCategories) { const tasks = floGlobals.appObjects[category].tasks || []; tasks.forEach(task => floGlobals.allAvailableTasks.add(task.id)) } for (const application in taskApplications) { const { message: { taskID }, senderID } = taskApplications[application]; if (!floGlobals.applications[taskID]) floGlobals.applications[taskID] = new Set() floGlobals.applications[taskID].add(senderID) } } else if (floGlobals.isAdmin) { } else { floGlobals.applications = new Set() const promises = [ floCloudAPI.requestGeneralData('taskApplications', { senderID: [floGlobals.myFloID, floGlobals.myBtcID], }), floCloudAPI.requestGeneralData('userProfile', { senderID: [floGlobals.myFloID, floGlobals.myBtcID], }) ] await Promise.all(promises) const taskApplications = floDapps.getNextGeneralData('taskApplications', '0'); floGlobals.allAvailableTasks = new Set(); for (const category in floGlobals.taskCategories) { const tasks = floGlobals.appObjects[category].tasks || []; tasks.forEach(task => floGlobals.allAvailableTasks.add(task.id)) } for (const application in taskApplications) { const { message: { taskID } } = taskApplications[application]; // if the task is still available, add it to the applications if (floGlobals.allAvailableTasks.has(taskID)) floGlobals.applications.add(taskID) } const userProfile = floDapps.getNextGeneralData('userProfile', '0'); floGlobals.userProfile = Object.values(userProfile).at(-1)?.message.encryptedData; } if (['#/landing', '#/sign_in', '#/sign_up'].includes(window.location.hash)) { history.replaceState(null, null, '#/home') router.routeTo('home') } else { router.routeTo(window.location.hash) } } catch (err) { console.error(err) } }).catch(error => console.error(error)) }); // We can allow users to send updates regarding the task. // handle task deadlines // Add icons to the task categories // handle applicants data securely (encrypted) and allow sub-admins to view them // ability to save data of interns which are promising beyond 7 days // ability to mark tasks which are delayed // have unified view for all tasks in subadmin view // stop user from sending multiple updates if previous update is not resolved