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) } buttonLoader('task_popup__submit', true) 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(() => { 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 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.isUserLoggedIn) { if (floGlobals.isSubAdmin) { actions = html` ${floGlobals.applications[id]?.size || 0} applied ` } else if (!floGlobals.isAdmin) { const applied = floGlobals.applications.has(id) actions = html` ` } } else { actions = html`Apply` floGlobals.applyingForTask = id; } return html`
  • ${title}

    ${description}

    ${actions}
  • ` }, availableTasks(options = {}) { const { type } = options if ((floGlobals.appObjects?.rmInterns.tasks || []).length === 0) return renderElem(getRef('available_tasks_list'), html`

    No tasks available

    `); let tasksList = floGlobals.appObjects.rmInterns.tasks; if (type) { if (type === 'applications') tasksList = tasksList.filter(task => floGlobals.applications.has(task.id)) else if (type === 'available') tasksList = tasksList.filter(task => !floGlobals.applications.has(task.id)) } tasksList = tasksList.map(render.task); renderElem(getRef('available_tasks_list'), 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 = () => { const { page } = router.state const isUserLoggedIn = page === 'loading' || floGlobals.isUserLoggedIn; 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; const interestedCategories = localStorage.getItem('interestedCategories') || '[]'; floGlobals.interestedCategories = new Set(JSON.parse(interestedCategories)); 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() { 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 ? 'applications' : 'available' } } = state; if (floGlobals.applyingForTask) { applyToTask(floGlobals.applyingForTask) floGlobals.applyingForTask = null; } else { renderElem(getRef('app_body'), html`
    ${header()}

    Home

    ${floGlobals.applications?.size > 0 ? html` Applied ${floGlobals.applications.size} Available ${floGlobals.appObjects.rmInterns.tasks.length - floGlobals.applications.size} ` : 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) { ({ name, email, college, course, whatsappNumber }) = JSON.parse(floDapps.user.decipher(floGlobals.userProfile)); } 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")); } }); floDapps.setMidStartup(() => new Promise((resolve, reject) => { Promise.all([ floCloudAPI.requestObjectData('rmInterns'), floCloudAPI.requestObjectData('c00'), floCloudAPI.requestObjectData('c01'), ]) .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 // alternating between floID and btcID every 10 seconds setInterval(() => { getRef('user_profile_id').textContent = showingFloID ? floGlobals.myBtcID : floGlobals.myFloID showingFloID = !showingFloID }, 10000) try { if (floGlobals.isSubAdmin) { const promises = [] 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 = {} 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'); for (const application in taskApplications) { const { message: { taskID } } = taskApplications[application]; if ((floGlobals.appObjects.rmInterns.tasks || []).some(task => task.id === 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)) }); // NOTE: Should we allow users to start working on a task without applying? // If yes, then we can allow users to send updates regarding the task. // handle task deadlines // should we allow users to apply for multiple tasks? // Add icons to the task categories // handle applicants data securely (encrypted) and allow sub-admins to view them // make separate object data for each categories and show only interested tasks // 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 // add option to publish certificates from RIBC