diff --git a/index.html b/index.html index d9afc69..905e33a 100644 --- a/index.html +++ b/index.html @@ -456,7 +456,9 @@
Pending
-

No pending requests

+
+

No pending requests

+
@@ -756,9 +758,8 @@

- + -
@@ -814,2442 +815,8 @@ - - - + + \ No newline at end of file diff --git a/scripts/app_ui.js b/scripts/app_ui.js new file mode 100644 index 0000000..2183808 --- /dev/null +++ b/scripts/app_ui.js @@ -0,0 +1,1608 @@ + floGlobals.taskCategories = { + c00: 'Creative Writing', + c01: 'Marketing', + c02: 'Design', + c03: 'Development', + c04: 'Social Media Management', + c05: 'Video Making', + } + const render = { + displayTaskCard(projectCode, branch, task) { + projectCode = projectCode + const taskDetails = { title, description, category, maxSlots, duration, durationType, reward } = RIBC.getTaskDetails(projectCode, branch, task) + return html` +
  • + +

    ${title}

    +
    + ${duration ? html` +
    + Duration: + ${duration} ${durationType} +
    + `: ''} + ${maxSlots ? html` +
    + Slots: + ${maxSlots} +
    + `: ''} + ${reward ? html` +
    + Reward: + ₹${reward} +
    + `: ''} +
    +
  • + `; + }, + displayTasks(category, searchQuery) { + // render tasks + const allTasks = RIBC.getAllTasks() + const filterCategory = category === 'all' ? false : category; + const filtered = [] + const availableCategories = new Set(); + for (const taskId in allTasks) { + const [projectCode, branch, task] = taskId.split('_') + if (filterCategory && allTasks[taskId].category !== filterCategory) continue; + if (RIBC.getTaskStatus(projectCode, branch, task) !== 'incomplete') continue; + if (searchQuery && searchQuery !== '' && !allTasks[taskId].title.toLowerCase().includes(searchQuery.toLowerCase())) continue; + if (RIBC.getAssignedInterns(projectCode, branch, task).length >= allTasks[taskId].maxSlots) continue; + if (typeOfUser && typeOfUser === 'intern' && floDapps.user.id && RIBC.getAssignedInterns(projectCode, branch, task).includes(floDapps.user.id)) continue; + filtered.push(render.displayTaskCard(projectCode, branch, task)) + availableCategories.add(allTasks[taskId].category) + } + let renderedTasks = filtered.reverse() + if (searchQuery && filtered.length === 0) { + renderedTasks = html`

    No tasks related to ${searchQuery}

    ` + } + // render categories + let renderedCategories = [] + if (availableCategories.size > 1) { + renderedCategories = [html`All`]; + availableCategories.forEach(categoryID => { + categories.push(html`${floGlobals.taskCategories[categoryID]}`) + }) + } + setTimeout(() => { + if (document.getElementById('task_search_input') && document.getElementById('task_search_input').value.trim() !== searchQuery) + document.getElementById('task_search_input').value = searchQuery || '' + }, 0); + return html` +
    +
    +

    + Available Tasks +

    + ${(filtered.length > 0 || searchQuery) ? html` + + + + `: ''} +
    + ${availableCategories.size > 1 ? html`${renderedCategories}` : ''} +
    +
    + +
    +

    Nothing to see here

    +
    +
    + `; + }, + projectCard(projectCode, isAdmin = false, ref) { + const projectName = RIBC.getProjectDetails(projectCode).projectName + const page = isAdmin ? 'admin_page' : 'project_explorer' + return html.for(ref, projectCode)`${projectName}` + }, + taskCard(task) { + const taskDetails = { title, description, category, maxSlots, duration, durationType, reward } = RIBC.getTaskDetails(appState.params.id, appState.params.branch, task) + const branches = getAllBranches(appState.params.id) + const branchesButtons = filterMap(branches, (branch) => { + const { branchName, parentBranch, startPoint, endPoint } = branch + if (parentBranch === appState.params.branch && startPoint === task) { + return render.branchButton({ + projectCode: appState.params.id, + branch: branchName, + page: 'project_explorer' + }) + } + }) + const assignedInterns = RIBC.getAssignedInterns(appState.params.id, appState.params.branch, task) || [] + const assignedInternsCards = filterMap(assignedInterns, (internFloId) => render.assignedInternCard(internFloId)); + const status = RIBC.getTaskStatus(appState.params.id, appState.params.branch, task) + let applyButton + if (!assignedInterns.includes(myFloID) && typeOfUser !== 'admin') { + const hasApplied = [...RIBC.getTaskRequests(false), ...sessionTaskRequests].find(({ details }) => { + return `${appState.params.id}_${appState.params.branch}_${task}` === details.taskId + }) + applyButton = html` + `; + } + const linkifyDescription = createElement('p', { + innerHTML: DOMPurify.sanitize(linkify(description)), + className: `timeline-task__description ws-pre-line wrap-around` + }) + return html` +
    +
    +
    + +
    +
    +
    +
    +
    +

    ${title}

    + ${applyButton} +
    + ${assignedInternsCards.length ? html`
    ${assignedInternsCards}
    ` : ''} +
    + Task details + ${linkifyDescription} +
    +
    + ${duration ? html` +
    + Duration: + ${duration} ${durationType} +
    + `: ''} + ${maxSlots ? html` +
    + Slots: + ${maxSlots} +
    + `: ''} + ${reward ? html` +
    + Reward: + ₹${reward} +
    + `: ''} + ${branchesButtons.length ? html`
    ${branchesButtons}
    ` : ''} +
    +
    + `; + }, + internCard(internFloId, { selectable = false } = {}) { + const internName = RIBC.getInternList()[internFloId] + const internPoints = RIBC.getInternRating(internFloId) + const initials = internName.split(' ').map(v => v.charAt(0)).join(''); + return html` + `; + }, + internUpdateCard(update) { + const { floID, time, note, update: { projectCode, branch, task, description, link } } = update + let topic = `${RIBC.getProjectDetails(projectCode).projectName} / ${RIBC.getTaskDetails(projectCode, branch, task).title}` + const internName = RIBC.getInternList()[floID] + let replyButton + if (typeOfUser === 'admin' && !note) { + replyButton = html`` + } + let providedLink + if (link) { + providedLink = html`${link} ` + } + let adminReply + if (note) { + adminReply = html`
    +

    Admin

    +

    ${note}

    +
    ` + } + return html.node` +
  • +
    + ${internName} + ${getFormattedTime(time)} +
    +

    ${topic}

    +

    ${description}

    + ${providedLink} + ${replyButton} + ${adminReply} +
  • `; + }, + branchButton(obj = {}) { + const { projectCode, branch, page, active = false } = obj + return html` + + ${branch} + + `; + }, + assignedInternCard(internFloId, options) { + let optionsButton + if (options) { + optionsButton = html` `; + } + return html` + + ${RIBC.getInternList()[internFloId]} + ${optionsButton} + + ` + }, + taskListItem(task, ref) { + const assignedInterns = RIBC.getAssignedInterns(appState.params.id, appState.params.branch, task) + const taskDetails = { title, description, category, maxSlots, duration, durationType, reward } = RIBC.getTaskDetails(appState.params.id, appState.params.branch, task) + const status = RIBC.getTaskStatus(appState.params.id, appState.params.branch, task) + let assignedInternsCards + if (assignedInterns) { + assignedInternsCards = filterMap(assignedInterns, (internFloId) => render.assignedInternCard(internFloId, true)) + } + const branches = getAllBranches(appState.params.id) + const branchesButtons = filterMap(branches, (branch) => { + const { branchName, parentBranch, startPoint, endPoint } = branch + if (parentBranch === appState.params.branch && startPoint === task) { + return render.branchButton({ + projectCode: appState.params.id, + branch: branchName, + page: 'admin_page' + }) + } + }) + const categories = []; + for (const categoryID in floGlobals.taskCategories) { + categories.push(html`${floGlobals.taskCategories[categoryID]}`) + } + const taskDescription = createElement('p', { + className: 'task-description ws-pre-line wrap-around', + attributes: { + 'data-editable': '', + 'data-edit-field': 'description', + }, + innerHTML: DOMPurify.sanitize(description) + }) + return html.for(ref, `${appState.params.id}_${appState.params.branch}_${task}`)` +
  • +
    +
    + +

    Mark as complete

    +
    +
    ID: ${task}
    +
    + + +
    +

    ${title}

    +
    + + ${assignedInternsCards} +
    + ${taskDescription} +
    + ${categories} +
    + + + + + Days + Months + +
    + + + + +
    + ${branchesButtons.length ? html`
    ${branchesButtons}
    ` : ''} +
  • + `; + }, + taskRequestCard(request) { + const { details: { taskId, name, brief, contact, portfolioLink }, floID, vectorClock } = request + const internName = RIBC.getInternList()[floID]; + const [projectCode, branch, task] = taskId.split('_'); + const { category } = RIBC.getTaskDetails(projectCode, branch, task); + return html` +
  • +
    ${floGlobals.taskCategories[category]}
    +

    + ${internName || name} applied for + ${RIBC.getTaskDetails(projectCode, branch, task).title} +

    + ${!internName ? html` +
    + ${brief ? html` +
    +
    Educational background
    +

    ${brief}

    +
    + ` : ''} + ${contact ? html` +
    +
    Contact
    + +
    + ` : ''} + ${portfolioLink ? html` +
    +
    Portfolio link
    + ${portfolioLink} +
    + ` : ''} +
    + ` : ''} +
    + + +
    +
  • + `; + }, + internTaskCard(uniqueId) { + const [projectCode, branch, task] = uniqueId.split('_'); + const { title, description } = RIBC.getTaskDetails(projectCode, branch, task) + const projectName = RIBC.getProjectDetails(projectCode).projectName + const linkifyDescription = createElement('p', { + innerHTML: DOMPurify.sanitize(linkify(description)), + className: `timeline-task__description ws-pre-line wrap-around` + }) + return html` +
  • + ${projectName} +
    +

    ${title}

    + ${linkifyDescription} +
    + +
  • + `; + }, + dashProject(projectCode, ref) { + const { projectName } = RIBC.getProjectDetails(projectCode) + const projectMap = RIBC.getProjectMap(projectCode) + const projectTasks = [] + RIBC.getProjectBranches(projectCode).forEach(branch => { + projectMap[branch].slice(4).forEach((task) => { + projectTasks.push(RIBC.getTaskStatus(projectCode, branch, task)) + }) + }) + const completedTasks = projectTasks.filter(task => task === 'completed').length + let completePercent = parseFloat(((completedTasks / (projectTasks.length || 1)) * 100).toFixed(2)) + const isPinned = pinnedProjects.includes(projectCode); + let pinIcon = '' + if (isPinned) { + pinIcon = html` `; + } else { + pinIcon = html` `; + } + return html.for(ref, projectCode)` +
    +
    + +
    +
    + +

    ${projectName}

    + +
    + +
    +
    +
    +
    + ${completePercent}% complete +
    + ` + }, + dashProjects(where, projects) { + renderElem(where, html`${projects.map(project => render.dashProject(project, where))} `) + }, + internRequests() { + const requestCategories = new Set() + const requestProjects = new Set() + const shouldFilterByProject = getRef('filter_requests_by_project').value !== 'all' ? getRef('filter_requests_by_project').value : false; + const shouldFilterByCategory = getRef('filter_requests_by_category').value !== 'all' ? getRef('filter_requests_by_category').value : false; + let requestCards = filterMap(RIBC.getTaskRequests().reverse(), (request) => { + if (Array.isArray(request.details) || !request.details.taskId) return; + const [projectCode, branch, task] = request.details.taskId.split('_') + const taskDetails = RIBC.getTaskDetails(projectCode, branch, task) + if (!taskDetails) return; + requestCategories.add(RIBC.getTaskDetails(projectCode, branch, task).category) + requestProjects.add(projectCode) + if (shouldFilterByCategory && taskDetails.category !== shouldFilterByCategory) return; + if (shouldFilterByProject && projectCode !== shouldFilterByProject) return; + return render.taskRequestCard(request) + }) + renderElem(getRef('requests_list'), html`${requestCards}`) + if (requestCategories.size) { + const categoryOptions = [...requestCategories].map(cat => html`${floGlobals.taskCategories[cat]}`); + renderElem(getRef('filter_requests_by_category'), html`${[html`All`, ...categoryOptions]}`) + } + if (requestProjects.size) { + const projectOptions = [...requestProjects].map(project => html`${RIBC.getProjectDetails(project).projectName}`); + renderElem(getRef('filter_requests_by_project'), html`${[html`All`, ...projectOptions]}`) + } + if (requestCategories.size || requestProjects.size) { + getRef('requests_container__filters').classList.remove('hidden') + } else { + getRef('requests_container__filters').classList.add('hidden') + } + }, + projectList(container, projects, isAdminList = false) { + renderElem(container, html`${projects.map(projectCode => render.projectCard(projectCode, isAdminList, container))}`) + }, + requestStatus(request) { + if (Array.isArray(request.details) || !request.details.taskId) return + const { details: { taskId }, status, vectorClock } = request; + const [projectCode, branch, task] = taskId.split('_'); + if (!RIBC.getTaskDetails(projectCode, branch, task)) return + const timestamp = parseInt(vectorClock.split('_')[0]) + let icon = '' + if (status === 'Accepted') { + icon = html`` + } else if (status === 'Rejected') { + icon = html`` + } else { + icon = html`` + } + return html` +
  • + +

    + You applied for ${RIBC.getTaskDetails(projectCode, branch, task).title} +

    +
    + ${icon} + ${status || 'Under review'} +
    +
  • + `; + }, + taskApplications() { + const taskRequests = RIBC.getTaskRequests(false) + taskRequests.sort((a, b) => { + return parseInt(b.vectorClock.split('_')[0]) - parseInt(a.vectorClock.split('_')[0]) + }) + const taskCards = filterMap(taskRequests, request => render.requestStatus(request)) + renderElem(getRef('task_requests_list'), html`${taskCards}`) + } + } + const selectedColors = [ + '--dark-red', + '--red', + '--kinda-pink', + '--purple', + '--shady-blue', + '--nice-blue', + '--maybe-cyan', + '--teal', + '--mint-green', + '--greenish-yellow', + '--yellowish-green', + '--dark-teal', + '--orange', + '--tangerine', + '--redish-orange', + ] + function randomColor() { + return selectedColors[Math.floor(Math.random() * selectedColors.length)]; + } + const renderedIntensColor = {} + function getInternColor(floId) { + if (!renderedIntensColor[floId]) { + renderedIntensColor[floId] = randomColor() + } + return renderedIntensColor[floId] + } + + const filterTasks = debounce((e) => { + const searchQuery = getRef('task_search_input').value.trim(); + const category = getRef('task_category_selector')?.value || 'all'; + window.location.hash = `#/${appState.currentPage}?category=${category}${searchQuery !== '' ? `&search=${searchQuery}` : ''}`; + }, 100) + + function showTaskDetails(taskId) { + const [projectCode, branch, task] = taskId.split('_') + const { title, description, category, maxSlots, duration, durationType, reward } = RIBC.getTaskDetails(projectCode, branch, task) + let hasApplied = false + try { + floDapps.user.id + hasApplied = [...RIBC.getTaskRequests(false), ...sessionTaskRequests].find(({ details }) => { + return taskId === details.taskId + }) + } catch (e) { } + const descriptionTag = createElement('p', { + innerHTML: DOMPurify.sanitize(linkify(description)), + className: 'ws-pre-line wrap-around' + }) + descriptionTag.id = 'task_description' + renderElem(getRef('task_details_wrapper'), html` +
    + +
    +
    +
    ${floGlobals.taskCategories[category]}
    +

    ${title}

    +
    + ${duration ? html` +
    + Duration: + ${duration} ${durationType} +
    + `: ''} + ${maxSlots ? html` +
    + Slots: + ${maxSlots} +
    + `: ''} + ${reward ? html` +
    + Reward: + ₹${reward} +
    + `: ''} +
    + ${descriptionTag} +
    + ${!hasApplied ? html` + + `: ''} + `); + getRef('task_details').classList.remove('hidden') + const animOptions = { + duration: floGlobals.prefersReducedMotion ? 0 : 300, + easing: 'ease', + fill: 'forwards' + } + getRef('task_details__backdrop').animate([ + { opacity: 0 }, + { opacity: 1 } + ], animOptions) + getRef('task_details_wrapper').animate([ + { transform: 'translateX(100%)' }, + { transform: 'translateX(0)' } + ], animOptions) + if (appState.currentPage === 'landing') { + getRef('landing').animate([ + { transform: 'translateX(0)' }, + { transform: 'translateX(-10%)' } + ], animOptions) + } + } + function hideTaskDetails() { + if (getRef('task_details').classList.contains('hidden')) return; + history.replaceState(null, null, `#/${appState.currentPage}`); + const animOptions = { + duration: floGlobals.prefersReducedMotion ? 0 : 300, + easing: 'ease', + fill: 'forwards' + } + getRef('task_details__backdrop').animate([ + { opacity: 1 }, + { opacity: 0 } + ], animOptions).onfinish = () => { + getRef('task_details').classList.add('hidden') + renderElem(getRef('task_details_wrapper'), html``) + } + getRef('task_details_wrapper').animate([ + { transform: 'translateX(0)' }, + { transform: 'translateX(100%)' } + ], animOptions) + if (appState.currentPage === 'landing') { + getRef('landing').animate([ + { transform: 'translateX(-10%)' }, + { transform: 'translateX(0)' }, + ], animOptions) + } + } + + let pinnedProjects = []; + let currentIntern; + let typeOfUser = 'general'; + + function handleDashboardViewChange(e) { + document.querySelectorAll('.dashboard-view__item').forEach(item => { + if (item.id === 'best_interns_container') + item.classList.add('hide-on-mobile') + else + item.classList.add('hidden') + }) + document.querySelector(`#${e.target.value}`).classList.remove('hide-on-mobile', 'hidden') + } + + + // Adds interns to the database **Only SubAdmins can add interns + function addInternToList() { + let internName = getRef('intern_name_field').value.trim(), + internFloId = getRef('intern_flo_id_field').value.trim(); + if (RIBC.admin.addIntern(internFloId, internName)) { + renderElem(getRef('admin_page__intern_list'), filterInterns('')) + closePopup(); + notify(`${internName} added as an intern.`, 'success') + } + } + function addProjectToList() { + let projectName = getRef('project_name_field').value.trim(), + projectDescription = getRef('project_description_field').value.trim(); + if (projectName === '') { + return notify('Project name is important!', 'error') + } + if (projectDescription === '') { + return notify('Project description is important!', 'error') + } + const projectCode = `${new Date().getFullYear()}-project-${RIBC.getProjectList() ? (RIBC.getProjectList().length + 1) : '1'}`; + RIBC.admin.createProject(projectCode) + RIBC.admin.addProjectDetails(projectCode, { projectName, projectDescription }) + render.projectList(getRef('admin_page__project_list'), getSortedProjectList(), true) + getRef('admin_page__project_list').querySelector(`[href="#/admin_page/project?id=${projectCode}&branch=mainLine"]`)?.click() + closePopup(); + } + + function makeEditable(elem) { + floGlobals.tempEditableContent = DOMPurify.sanitize(elem.innerHTML.trim()) + elem.contentEditable = true + elem.focus() + document.execCommand('selectAll', false, null); + } + + getRef('project_details_wrapper').addEventListener('dblclick', e => { + if (e.target.closest('[data-editable]') && !e.target.closest('[data-editable]').isContentEditable) { + makeEditable(e.target.closest('[data-editable]')) + } + }) + getRef('project_details_wrapper').addEventListener('focusout', (e) => { + if (e.target.isContentEditable) { + e.target.contentEditable = false + if (e.target.innerHTML.trim() !== '' && floGlobals.tempEditableContent !== DOMPurify.sanitize(e.target.innerHTML.trim())) { + const newTitle = DOMPurify.sanitize(getRef('editing_panel__title').innerHTML.trim()) + const newDescription = DOMPurify.sanitize(getRef('editing_panel__description').innerHTML.trim()) + RIBC.admin.addProjectDetails(appState.params.id, { projectName: newTitle, projectDescription: newDescription }) + notify('Changes saved locally, commit the changes to make them permanent', 'success') + render.projectList(getRef('admin_page__project_list'), getSortedProjectList(), true) + } else { + e.target.innerHTML = floGlobals.tempEditableContent + } + } + }) + + // opens a popup containing various intern information + function showInternInfo(internFloId) { + const internName = RIBC.getInternList()[internFloId] + getRef('intern_info__initials').textContent = internName.split(' ').map(v => v.charAt(0)).join(''); + getRef('intern_info__initials').style.setProperty('--color', `var(${getInternColor(internFloId)})`) + getRef('intern_info__name').textContent = internName; + getRef('intern_info__flo_id').value = currentIntern = internFloId; + getRef('intern_info__score').textContent = RIBC.getInternRating(internFloId); // points earned by intern + if (RIBC.getInternRating(internFloId) === 1) { + getRef('reduce_score_button').disabled = true; + } + openPopup('intern_info_popup'); + } + + // opens a popup containing various project information + function showProjectInfo(projectCode) { + const { projectName, projectDescription } = RIBC.getProjectDetails(projectCode); + getRef('project_explorer__project_title').textContent = projectName; // project name + getRef('project_explorer__project_description').textContent = projectDescription; + getRef('project_explorer__project_updates').href = `#/updates_page?projectCode=${projectCode}&internId=all`; + renderBranches(); + } + + let currentTask = ''; + function renderAdminProjectView(projectCode) { + const allProjects = getRef('admin_page__project_list').querySelectorAll('.project-card'); + allProjects.forEach(project => project.classList.remove('project-card--active')) + const targetProject = Array.from(allProjects).find(project => project.getAttribute('href').includes(projectCode)) + if (targetProject) + targetProject.classList.add('project-card--active') + const { projectName, projectDescription } = RIBC.getProjectDetails(projectCode); + getRef('editing_panel__title').textContent = projectName; + getRef('editing_panel__description').textContent = projectDescription; + renderBranches() + } + function renderBranches() { + const { id: projectCode, branch } = appState.params + const taskListContainer = appState.currentPage === 'admin_page' ? 'branch_container' : 'explorer_branch_container'; + const branchList = filterMap(RIBC.getProjectBranches(appState.params.id), (branch) => { + return render.branchButton({ projectCode, branch, page: appState.currentPage, active: branch === appState.params.branch }) + }) + if (branchList.length > 1) { + renderElem(getRef(taskListContainer), html`${branchList}`) + getRef(taskListContainer).classList.remove('hidden') + } else { + getRef(taskListContainer).classList.add('hidden') + } + } + function renderBranchTasks() { + const { id: projectCode, branch } = appState.params + const taskListContainer = appState.currentPage === 'admin_page' ? 'task_list' : 'explorer_task_list'; + let branchTasks = RIBC.getProjectMap(appState.params.id)[appState.params.branch]; + if (branchTasks[1] && !taskListContainer === 'task_list') { + getRef(taskListContainer).textContent = "No tasks added yet, Please explore other projects" + } else { + let tasks = [] + if (branch !== 'mainLine') { + const { startPoint, parentBranch } = getAllBranches(projectCode).find(({ branchName }) => branchName === branch) + tasks.push(html`

    + Branched off from ${parentBranch} +

    `) + } + if (taskListContainer === 'task_list') { + branchTasks.slice(4).forEach((task) => tasks.push(render.taskListItem(task, getRef(taskListContainer)))) + } else { + branchTasks.slice(4).forEach((task) => tasks.push(render.taskCard(task))) + } + renderElem(getRef(taskListContainer), html`${tasks}`) + } + } + function getAllBranches(projectCode) { + const projectMap = RIBC.getProjectMap(projectCode) + const projectBranches = RIBC.getProjectBranches(projectCode) + return projectBranches.slice(1).map((branchName, index) => { + const [parentBranch, , startPoint, endPoint] = projectMap[branchName] + return { + branchName, + parentBranch, + startPoint, + endPoint + } + }) + } + + let currentViewIndex = 0; + getRef('admin_view_selector').addEventListener('change', (e) => { + const newViewIndex = parseInt(e.target.value); + showChildElement(getRef('admin_views'), newViewIndex, { entry: newViewIndex > currentViewIndex ? slideInLeft : slideInRight, exit: newViewIndex > currentViewIndex ? slideOutLeft : slideOutRight }); + currentViewIndex = parseInt(e.target.value); + }) + + function toggleEditing(target) { + if (target === 'title') { + makeEditable(currentTask.querySelector('.task-title')) + } else { + makeEditable(currentTask.querySelector('.task-description')) + } + } + function formatAmount(amount = 0, currency = 'inr') { + if (!amount) + return '₹0'; + return amount.toLocaleString(currency === 'inr' ? `en-IN` : 'en-US', { style: 'currency', currency, maximumFractionDigits: 0 }) + } + delegate(getRef('task_list'), 'change', 'sm-checkbox', (e) => { + currentTask = e.target.closest('.task-list-item'); + const taskStatus = e.target.checked ? 'completed' : 'incomplete' + RIBC.admin.putTaskStatus(taskStatus, appState.params.id, appState.params.branch, currentTask.dataset.taskId) + }) + delegate(getRef('task_list'), 'change', 'sm-select', (e) => { + currentTask = e.target.closest('.task-list-item'); + const taskDetails = { + [e.target.dataset.editField]: e.target.value + } + RIBC.admin.editTaskDetails(taskDetails, appState.params.id, appState.params.branch, currentTask.dataset.taskId) + notify('Changes saved locally, commit the changes to make them permanent', 'success') + }) + getRef('task_list').addEventListener('focusout', (e) => { + currentTask = e.target.closest('.task-list-item'); + if (!currentTask) return; + const ogTaskDetails = RIBC.getTaskDetails(appState.params.id, appState.params.branch, currentTask.dataset.taskId) + const newTaskDetails = {} + let valid = false; + if (e.target.isContentEditable) { + e.target.contentEditable = false + newTaskDetails[e.target.dataset.editField] = DOMPurify.sanitize(e.target.innerHTML.trim()) + valid = true; + } else if (e.target.closest('sm-input')) { + newTaskDetails[e.target.dataset.editField] = parseInt(e.target.value) + valid = true; + } + if (!valid) return; + if (ogTaskDetails[e.target.dataset.editField] !== newTaskDetails[e.target.dataset.editField]) { + RIBC.admin.editTaskDetails(newTaskDetails, appState.params.id, appState.params.branch, currentTask.dataset.taskId) + notify('Changes saved locally, commit the changes to make them permanent', 'success') + } + }) + getRef('task_list').addEventListener('dblclick', (e) => { + if (e.target.closest('[data-editable]') && !e.target.closest('[data-editable]').isContentEditable) { + makeEditable(e.target.closest('[data-editable]')) + } + }) + getRef('task_list').addEventListener('click', (e) => { + if (e.target.closest('.task-list-item')) { + currentTask = e.target.closest('.task-list-item'); + } + if (e.target.closest('.task-option')) { + const optionButton = e.target.closest('.task-option') + getRef('task_context').setAttribute('style', `top: ${optionButton.offsetTop}px`) + getRef('task_context').classList.remove('hidden') + getRef('task_context').animate([ + { + transform: 'scaleY(0.95) translateY(-0.5rem)', + opacity: '0' + }, + { + transform: 'none', + opacity: '1' + }, + ], { + duration: floGlobals.prefersReducedMotion ? 0 : 200, + easing: 'ease' + }) + .onfinish = () => { + getRef('task_context').firstElementChild.focus() + const y = document.addEventListener("click", function (e) { + if (e.target.closest('#context_menu') || e.target.closest('.task-option')) return; + getRef('task_context').animate([ + { + transform: 'none', + opacity: '1' + }, + { + transform: 'scaleY(0.95) translateY(-0.5rem)', + opacity: '0' + }, + ], { + duration: floGlobals.prefersReducedMotion ? 0 : 100, + easing: 'ease' + }).onfinish = () => { + getRef('task_context').classList.add('hidden') + document.removeEventListener('click', y); + } + }); + } + } + else if (e.target.closest('.assigned-intern button')) { + getConfirmation('Do you want to unassign this intern from this task?', { confirmText: 'Unassign' }).then((result) => { + if (result) { + RIBC.admin.unassignInternFromTask(e.target.closest('.assigned-intern').dataset.floId, appState.params.id, appState.params.branch, currentTask.dataset.taskId) + notify('Intern removed from the task') + renderBranchTasks() + } + }) + } + else if (e.target.closest('.cancel-task-button')) { + const card = e.target.closest('.temp-task') + card.remove(); + getRef('add_task').classList.remove('hidden') + } + else if (e.target.closest('.add-task-button')) { + const card = e.target.closest('.temp-task') + const title = card.querySelector('.temp-task__title').value.trim(); + const description = card.querySelector('.temp-task__description').value.trim(); + const category = card.querySelector('.temp-task__category').value.trim(); + const maxSlots = parseInt(card.querySelector('.temp-task__max-slots').value.trim()); + const duration = parseInt(card.querySelector('.temp-task__duration').value.trim()); + const durationType = card.querySelector('.temp-task__duration-type').value.trim(); + const reward = parseInt(card.querySelector('.temp-task__reward').value.trim()); + if (title === '') { + return notify('Please enter task title', 'error') + } + if (description === '') { + return notify('Please enter description of the task', 'error') + + } + const taskDetails = { + title, + description, + category, + maxSlots, + duration, + durationType, + reward + } + const task = RIBC.admin.addTaskInMap(appState.params.id, appState.params.branch) + RIBC.admin.editTaskDetails(taskDetails, appState.params.id, appState.params.branch, task) + RIBC.admin.putTaskStatus('incomplete', appState.params.id, appState.params.branch, task) + card.remove() + renderBranchTasks() + getRef('add_task').classList.remove('hidden') + notify('Task added to current branch', 'success') + } + }) + function addPlaceholderTask() { + const categories = []; + let first = true; + for (const categoryID in floGlobals.taskCategories) { + categories.push(html`${floGlobals.taskCategories[categoryID]}`) + first = false; + } + const placeholderTask = html.node` +
    + + + +
    + ${categories} + +
    + + + + + Days + Months + +
    + + + +
    +
    + + +
    +
    +
    + `; + getRef('task_list').append(placeholderTask) + getRef('task_list').querySelector('.temp-task__title').focusIn() + getRef('add_task').classList.add('hidden') + getRef('task_list').lastElementChild.scrollIntoView({ behavior: "smooth" }); + } + function commitToChanges() { + getConfirmation("Do you want to commit to changes?").then((result) => { + if (result) { + RIBC.admin.updateObjects().then(res => { + notify('Changes committed.', 'success') + }).catch(err => { + console.error(err) + }) + } + }) + } + function removeThisTask() { + getConfirmation("Are you sure to delete this task?", { confirmText: 'Delete' }).then((result) => { + if (result) { + RIBC.admin.deleteTaskInMap(appState.params.id, appState.params.branch, currentTask.dataset.taskId) + renderBranchTasks() + } + }) + } + floGlobals.selectedInterns = new Set() + delegate(getRef('intern_list_container'), 'change', '.intern-card', (e) => { + const floId = e.target.closest('.intern-card').dataset.internFloId; + if (e.target.checked) { + floGlobals.selectedInterns.add(floId) + } else { + floGlobals.selectedInterns.delete(floId) + } + getRef('assign_interns_button').disabled = !floGlobals.selectedInterns.size + }) + function assignSelectedInterns() { + floGlobals.selectedInterns.forEach(floId => { + RIBC.admin.assignInternToTask(floId, appState.params.id, appState.params.branch, currentTask.dataset.taskId) + renderBranchTasks() + }) + notify(`Assigned task`, 'success') + closePopup() + } + + function renderAllInterns() { + renderElem(getRef('all_interns_list'), filterInterns('', { sortByRating: true })) + } + + function changeScore(scoreUpdate) { + let score = parseInt(getRef('intern_info__score').textContent) + score += scoreUpdate; + getRef('intern_info__score').textContent = score + document.querySelectorAll(`[data-intern-flo-id="${currentIntern}"]`).forEach(internCard => { + internCard.querySelector('.intern-card__score').textContent = score + }) + if (score > 0) { + getRef('reduce_score_button').disabled = false; + RIBC.admin.updateInternRating(currentIntern, scoreUpdate) + } + if (score === 1 && scoreUpdate === -1) { + getRef('reduce_score_button').disabled = true; + } + } + + function showNewBranchPopup() { + openPopup('create_branch_popup') + const startPoint = parseInt(currentTask.dataset.taskId) + getRef('branch_start_point').value = startPoint; + } + getRef('create_branch_btn').onclick = () => { + const startPoint = parseInt(currentTask.dataset.taskId) + const userMergePoint = getRef('branch_merge_point').value.trim() + const mergePoint = (userMergePoint === '') ? startPoint : parseInt(userMergePoint) + const branchName = RIBC.admin.addBranch(appState.params.id, appState.params.branch, startPoint, mergePoint); + notify(`Branch added ${branchName}`, 'success') + renderBranches() + closePopup() + } + + function clearRequestFilters() { + getRef('filter_requests_by_category').reset() + getRef('filter_requests_by_project').reset() + } + + function renderProjectSelectorOptions() { + const options = [html`All`]; + RIBC.getProjectList().reverse().forEach(project => { + options.push(html`${RIBC.getProjectDetails(project).projectName}`); + }) + renderElem(getRef('updates_page__project_selector'), html`${options}`) + } + function renderInternSelectorOptions() { + const options = [html`All`]; + const allInterns = Object.entries(RIBC.getInternList()).sort((a, b) => a[1].toLowerCase().localeCompare(b[1].toLowerCase())); + allInterns.forEach(intern => { + options.push(html`${intern[1]}`); + }) + renderElem(getRef('updates_page__intern_selector'), html`${options}`) + } + + function getUpdatesByProject(projectCode) { + const projectName = RIBC.getProjectDetails(projectCode).projectName + const allUpdates = RIBC.getInternUpdates() + const filteredUpdates = allUpdates.filter(({ update: { projectCode: updateProjectCode } }) => { + return projectCode === updateProjectCode + }) + return filteredUpdates + } + + function getUpdatesByIntern(floId, allUpdates = RIBC.getInternUpdates()) { + return allUpdates.filter(update => update.floID === floId) + } + function getUpdatesByDate(date, allUpdates = RIBC.getInternUpdates()) { + const filteredUpdates = [] + const dateStart = new Date(`${date} 00:00:00`).getTime() + const dateEnd = new Date(`${date} 23:59:59`).getTime() + let isFromDate = false + for (const update of allUpdates) { + if (update.time > dateStart && update.time < dateEnd) { + filteredUpdates.push(update) + isFromDate = true + } else if (isFromDate) break + } + return filteredUpdates + } + let updatesLazyLoader + function renderInternUpdates(updates = RIBC.getInternUpdates()) { + if (updatesLazyLoader) { + updatesLazyLoader.update(updates) + } else { + updatesLazyLoader = new LazyLoader('#all_updates_list', updates, render.internUpdateCard) + } + updatesLazyLoader.init() + } + delegate(getRef('all_updates_list'), 'click', '.init-update-replay', (e) => { + const vectorClock = e.delegateTarget.closest('.intern-update').dataset.vectorClock; + e.delegateTarget.after(html.node` + + +
    + +
    + +
    +
    +
    + `) + e.delegateTarget.classList.add('hidden') + e.target.closest('.intern-update').querySelector('.update-reply-textarea').focusIn() + }) + + function cancelUpdateReply(replayBox) { + replayBox.previousElementSibling.classList.remove('hidden') + replayBox.remove() + } + function submitUpdateReply(replayBox) { + buttonLoader(replayBox.querySelector('.update-replay__submit'), true) + const vectorClock = replayBox.previousElementSibling.closest('.intern-update').dataset.vectorClock; + const replyText = replayBox.querySelector('.update-reply-textarea').value.trim() + if (replyText !== '') { + RIBC.admin.commentInternUpdate(vectorClock, replyText).then(res => { + replayBox.previousElementSibling.remove() + replayBox.replaceWith(html.node` +
    +

    Admin

    +

    ${replyText}

    +
    `) + }).catch(err => { + notify(err, 'error') + buttonLoader(replayBox.querySelector('.update-replay__submit'), false) + }) + } + } + function setUpdateFilters(filters) { + const { projectCode, internId, date } = filters || getUpdateFilters() + if (filters) { + getRef('updates_page__project_selector').value = projectCode + getRef('updates_page__intern_selector').value = internId + getRef('updates_page__date_selector').value = date || '' + } else { + const dateParam = date !== '' ? `&date=${date}` : '' + location.hash = `/updates_page?projectCode=${projectCode}&internId=${internId}${dateParam}` + } + } + function getUpdateFilters() { + const projectCode = getRef('updates_page__project_selector').value || 'all' + const internId = getRef('updates_page__intern_selector').value || 'all' + const date = getRef('updates_page__date_selector').value || '' + return { projectCode, internId, date } + } + + function clearUpdatesFilter() { + getRef('updates_page__project_selector').reset() + getRef('updates_page__intern_selector').reset() + getRef('updates_page__date_selector').value = '' + setUpdateFilters() + } + + getRef('updates_page__project_selector').addEventListener('change', e => setUpdateFilters()) + getRef('updates_page__intern_selector').addEventListener('change', e => setUpdateFilters()) + getRef('updates_page__date_selector').addEventListener('change', e => setUpdateFilters()) + function pinProject(thisBtn) { + const projectCode = thisBtn.closest('.pinned-card').dataset.id; + pinnedProjects = localStorage.getItem(`${myFloID}_pinned_projects`) ? localStorage.getItem(`${myFloID}_pinned_projects`).split(',') : [] + if (pinnedProjects.includes(projectCode)) { + pinnedProjects = pinnedProjects.filter(project => project !== projectCode) + } else { + pinnedProjects.push(projectCode) + } + localStorage.setItem(`${myFloID}_pinned_projects`, pinnedProjects.join()) + render.dashProjects(getRef('pinned_projects'), pinnedProjects) + const unpinnedProjects = RIBC.getProjectList().filter(project => !pinnedProjects.includes(project)).reverse() + if (unpinnedProjects.length > 0) { + getRef('project_list_container').classList.remove('hidden') + } else { + getRef('project_list_container').classList.add('hidden') + } + render.dashProjects(getRef('project_list'), unpinnedProjects) + } + + let sessionTaskRequests = new Set(); + function requestForTask(btn) { + hideTaskDetails() + try { + floDapps.user.id + const taskId = btn ? btn.dataset.taskId : floGlobals.tempUserTaskRequest + floGlobals.tempUserTaskRequest = taskId + if (typeOfUser === 'general') { + getRef('intern_apply__task').textContent = RIBC.getAllTasks()[taskId].title + openPopup('apply_for_task_popup', true) + } else if (typeOfUser === 'intern') { + const hasApplied = [...RIBC.getTaskRequests(false), ...sessionTaskRequests].find(({ details }) => { + return taskId === details.taskId + }) + if (hasApplied) { + notify('You have already applied for this task', 'error') + } else { + if (floGlobals.assignedTasks.has(taskId)) + return notify('You have already been assigned this task', 'error'); + const [projectCode, branch, task] = taskId.split('_') + const { title } = RIBC.getTaskDetails(projectCode, branch, task) + getConfirmation(`Do you want to apply for "${title}"`, { confirmText: 'Apply' }).then((result) => { + if (result) { + if (btn) { + btn.textContent = 'Applying...' + btn.disabled = true + } + RIBC.applyForTask({ taskId }).then((result) => { + notify('Applied successfully.', 'success') + sessionTaskRequests.add({ details: { taskId } }) + floGlobals.tempUserTaskRequest = null + btn.textContent = 'Applied' + }).catch((err) => { + if (btn) { + btn.textContent = 'Apply' + btn.disabled = false + } + notify(err, 'error') + }) + } + }).catch((error) => { + notify(error, 'error') + }) + } + } + } catch (err) { + floGlobals.tempUserTaskRequest = btn.dataset.taskId; + location.hash = '#/sign_in' + floGlobals.signInNotification = notify('Please login to apply for task.') + } + } + + function toggleUpdatesFilter() { + getRef('update_filters_wrapper').classList.toggle('hide-on-mobile') + } + // Event listeners + delegate(getRef('all_interns_page'), 'click', '.intern-card', e => { + showInternInfo(e.delegateTarget.dataset.internFloId) + }) + delegate(getRef('admin_page__intern_list'), 'click', '.intern-card', e => { + showInternInfo(e.delegateTarget.dataset.internFloId) + }) + + document.addEventListener('popupopened', e => { + getRef('main_page').setAttribute('inert', '') + switch (e.detail.popup.id) { + case 'intern_list_popup': + renderElem(getRef('intern_list_container'), filterInterns('', { availableInternsOnly: true })) + break; + } + }) + document.addEventListener('popupclosed', e => { + switch (e.detail.popup.id) { + case 'intern_list_popup': + renderElem(getRef('intern_list_container'), html``) + getRef('intern_search_field').value = '' + floGlobals.selectedInterns.clear() + getRef('assign_interns_button').disabled = true + break; + } + if (popupStack.items.length === 0) { + getRef('main_page').removeAttribute('inert') + } + }) + + floGlobals.assignedTasks = new Set() + + function renderAllElements() { + + let sortedProjectList = getSortedProjectList() + document.querySelectorAll('.open-first-project').forEach(link => { + link.href = `${link.href}/project?id=${sortedProjectList[0]}&branch=mainLine` + }) + + pinnedProjects = localStorage.getItem(`${myFloID}_pinned_projects`) ? localStorage.getItem(`${myFloID}_pinned_projects`).split(',') : [] + + // Intern's view + + if (RIBC.getInternList()[myFloID] && !floGlobals.subAdmins.includes(myFloID)) { + typeOfUser = 'intern'; + document.querySelectorAll('.intern-option').forEach((option) => { + option.classList.remove('hidden') + }) + floGlobals.assignedProjectsList = new Set(); + // store all the projects assigned to interns in array + const allTasks = RIBC.getAllTasks() + for (const taskKey in allTasks) { + const [projectCode, branch, task] = taskKey.split('_') + const assignedInterns = RIBC.getAssignedInterns(projectCode, branch, task) + if (Array.isArray(assignedInterns) && assignedInterns.includes(myFloID)) { + floGlobals.assignedProjectsList.add(projectCode) + if (RIBC.getTaskStatus(projectCode, branch, task) === 'incomplete') { + floGlobals.assignedTasks.add(taskKey); + } + } + } + } else { + document.querySelectorAll('.intern-option').forEach((option) => { + option.classList.add('hidden') + }) + } + + // admin view + if (floGlobals.subAdmins.includes(myFloID)) { + typeOfUser = 'admin' + function removeRequest(requestCard) { + requestCard.animate([ + { + transform: 'translateX(0)', + opacity: 1 + }, + { + transform: 'translateX(-100%)', + opacity: 0 + }, + ], { + duration: floGlobals.prefersReducedMotion ? 0 : 300, + easing: 'ease' + }).onfinish = () => { + requestCard.remove() + } + } + render.internRequests() + // accept task request + delegate(getRef('requests_list'), 'click', '.accept-request', (e) => { + getConfirmation('Are you sure you want to accept this request?').then(result => { + if (result) { + const vectorClock = e.delegateTarget.closest('.request-card').dataset.vectorClock + let result + if (RIBC.getInternList()) + result = RIBC.admin.processTaskRequest(vectorClock, true) + if (result === 'Accepted') { + notify('Intern assigned, commit changes to make it permanent.', 'success') + removeRequest(e.delegateTarget.closest('.request-card')) + } + } + }) + }) + // reject task request + delegate(getRef('requests_list'), 'click', '.reject-request', (e) => { + getConfirmation('Are you sure you want to reject this request?').then((result) => { + if (result) { + const vectorClock = e.delegateTarget.closest('.request-card').dataset.vectorClock + const type = e.delegateTarget.closest('.request-card').dataset.type + let result + if (type === 'task') { + result = RIBC.admin.processTaskRequest(vectorClock, false) + if (result === 'Rejected') { + notify('Request rejected', 'success') + removeRequest(e.delegateTarget.closest('.request-card')) + } + } else if (type === 'internship') { + result = RIBC.admin.processInternRequest(vectorClock, false) + if (result === 'Rejected') { + notify('Request rejected', 'success') + removeRequest(e.delegateTarget.closest('.request-card')) + } + } + } + }) + }) + + document.querySelectorAll('.admin-option').forEach((option) => { + option.classList.remove('hidden') + }) + + //show interns + renderElem(getRef('admin_page__intern_list'), filterInterns('')) + + //show projects + render.projectList(getRef('admin_page__project_list'), getSortedProjectList(), true) + } else { + document.querySelectorAll('.admin-option').forEach((option) => { + option.classList.add('hidden') + }) + } + + // General only view for non admin and non intern + if (!RIBC.getInternList()[myFloID] && !floGlobals.subAdmins.includes(myFloID)) { + document.querySelectorAll('.general-only').forEach((elem) => { + elem.classList.remove('hidden') + }) + } + else { + document.querySelectorAll('.general-only').forEach((elem) => { + elem.classList.add('hidden') + }) + } + if (typeOfUser === 'admin') { + document.querySelectorAll('.not-for-admin').forEach((elem) => { + elem.classList.add('hidden') + }) + } else { + document.querySelectorAll('.not-for-admin').forEach((elem) => { + elem.classList.remove('hidden') + }) + } + + if (typeOfUser === 'intern') { + render.projectList(getRef('my_projects'), [...floGlobals.assignedProjectsList]) + sortedProjectList = sortedProjectList.filter(val => !floGlobals.assignedProjectsList.has(val)); + } + if (sortedProjectList.length > 0) { + getRef('other_projects').previousElementSibling.classList.remove('hidden') + render.projectList(getRef('other_projects'), sortedProjectList) + } else { + getRef('other_projects').previousElementSibling.classList.add('hidden') + } + delegate(getRef('explorer_task_list'), 'click', '.apply-button', e => { + requestForTask(e.delegateTarget) + }) + getRef('user_flo_id').value = myFloID; + } + + let currentTaskId; + function initTaskUpdate(e) { + const taskCard = e.target.closest('.task-card') + currentTaskId = taskCard.dataset.uniqueId + const [projectCode, branch, task] = currentTaskId.split('_') + getRef('update_of_project').textContent = RIBC.getProjectDetails(projectCode).projectName + getRef('update_of_task').textContent = RIBC.getTaskDetails(projectCode, branch, task).title + openPopup('post_update_popup') + } + + function postUpdate() { + const [projectCode, branch, task] = currentTaskId.split('_') + const description = getRef('update__brief').value.trim() + const linkText = getRef('update__link').value.trim() + const link = linkText !== '' ? linkText : null + if (description !== '') { + RIBC.postInternUpdate({ projectCode, branch, task, description, link }) + .then((result) => { + notify('Update posted', 'success') + closePopup() + }) + .catch((error) => { + notify(error, 'error') + }) + } + else { + notify('Please enter description', 'error') + } + } + function filterInterns(searchKey, options = {}) { + const { + sortByRating = false, + availableInternsOnly = false + } = options + let filtered = []; + const allInterns = RIBC.getInternList(); + const highPerformingInterns = Object.keys(allInterns).sort((a, b) => { + return RIBC.getInternRating(b) - RIBC.getInternRating(a) + }); + let arrayOfInterns = Object.keys(allInterns).sort((a, b) => { + return allInterns[a].toLowerCase().localeCompare(allInterns[b].toLowerCase()) + }) + if (availableInternsOnly) { + arrayOfInterns = arrayOfInterns.filter(intern => !RIBC.getAssignedInterns(appState.params.id, appState.params.branch, currentTask.dataset.taskId)?.includes(intern)) + } + if (searchKey === '') { + filtered = (sortByRating ? highPerformingInterns : arrayOfInterns).map(floId => { + return render.internCard(floId, { selectable: availableInternsOnly }) + }) + } else { + filtered = filterMap(arrayOfInterns, (floId) => { + if (allInterns[floId].toLowerCase().includes(searchKey.toLowerCase())) { + return render.internCard(floId, { selectable: availableInternsOnly }) + } + }) + } + return html`${filtered}` + } + const searchInternPopup = debounce((e) => { + renderElem(getRef('intern_list_container'), filterInterns(e.target.value.trim(), { availableInternsOnly: true })) + }, 150) + const searchInternPage = debounce((e) => { + renderElem(getRef('all_interns_list'), filterInterns(e.target.value.trim(), { sortByRating: true })) + }, 150) + getRef('intern_search_field').addEventListener('input', searchInternPopup) + getRef('interns_page__search').addEventListener('input', searchInternPage) + + + function applyForInternship() { + buttonLoader(getRef('intern_apply__button'), true) + const name = getRef('intern_apply__name').value.trim(); + const contact = getRef('intern_apply__contact').value.trim(); + const brief = getRef('intern_apply__brief').value.trim(); + // const resumeLink = getRef('intern_apply__resume_link').value.trim(); + const portfolioLink = getRef('intern_apply__portfolio_link').value.trim(); + const details = { + name, + brief, + // resumeLink, + contact, + portfolioLink: portfolioLink !== '' ? portfolioLink : null, + taskId: floGlobals.tempUserTaskRequest + } + RIBC.applyForTask(details) + .then((result) => { + notify('Application submitted', 'success') + closePopup() + }) + .catch((error) => { + notify(error, 'error') + }).finally(() => { + buttonLoader(getRef('intern_apply__button'), false) + floGlobals.tempUserTaskRequest = null + }) + } + + function getSortedProjectList() { + return RIBC.getProjectList().sort((a, b) => RIBC.getProjectDetails(a).projectName.toLowerCase().localeCompare(RIBC.getProjectDetails(b).projectName.toLowerCase())) + } + + + function getSignedIn(passwordType) { + return new Promise((resolve, reject) => { + try { + getPromptInput('Enter password', '', { + isPassword: true, + }).then(password => { + if (password) { + resolve(password) + } + }) + } catch (err) { + if (passwordType === 'PIN/Password') { + floGlobals.isPrivKeySecured = true; + getRef('private_key_field').removeAttribute('data-private-key'); + getRef('private_key_field').setAttribute('placeholder', 'Password'); + getRef('private_key_field').customValidation = null + getRef('secure_pwd_button').closest('.card').classList.add('hidden'); + } else { + floGlobals.isPrivKeySecured = false; + getRef('private_key_field').dataset.privateKey = '' + getRef('private_key_field').setAttribute('placeholder', 'FLO private key'); + getRef('private_key_field').customValidation = floCrypto.getPubKeyHex; + getRef('secure_pwd_button').closest('.card').classList.remove('hidden'); + } + if (!generalPages.find(page => window.location.hash.includes(page))) { + location.hash = floGlobals.isPrivKeySecured ? '#/sign_in' : `#/landing`; + } + getRef('sign_in_button').onclick = () => { + resolve(getRef('private_key_field').value.trim()); + getRef('private_key_field').value = ''; + routeTo('loading'); + getRef("notification_drawer").remove(floGlobals.signInNotification) + }; + getRef('sign_up_button').onclick = () => { + resolve(getRef('generated_private_key').value); + getRef('generated_private_key').value = ''; + routeTo('loading'); + getRef("notification_drawer").remove(floGlobals.signInNotification) + }; + } + }); + } + 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'); + getRef('secure_pwd_button').closest('.card').classList.add('hidden'); + 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(); + } + }); + } + // detect url within text and convert to link + function linkify(inputText) { + let replacedText, replacePattern1, replacePattern2, replacePattern3; + //URLs starting with http://, https://, or ftp:// + replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; + replacedText = inputText.replace(replacePattern1, '$1'); + //URLs starting with "www." (without // before it, or it'd re-link the ones done above). + replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim; + replacedText = replacedText.replace(replacePattern2, '$1$2'); + //Change email addresses to mailto:: links. + replacePattern3 = /(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,6})/gim; + replacedText = replacedText.replace(replacePattern3, '$1'); + return replacedText; + } \ No newline at end of file diff --git a/scripts/std_ui.js b/scripts/std_ui.js new file mode 100644 index 0000000..ba4a510 --- /dev/null +++ b/scripts/std_ui.js @@ -0,0 +1,819 @@ +/*jshint esversion: 8 */ +/** + * @yaireo/relative-time - javascript function to transform timestamp or date to local relative-time + * + * @version v1.0.0 + * @homepage https://github.com/yairEO/relative-time + */ + +!function (e, t) { var o = o || {}; "function" == typeof o && o.amd ? o([], t) : "object" == typeof exports && "object" == typeof module ? module.exports = t() : "object" == typeof exports ? exports.RelativeTime = t() : e.RelativeTime = t() }(this, (function () { const e = { year: 31536e6, month: 2628e6, day: 864e5, hour: 36e5, minute: 6e4, second: 1e3 }, t = "en", o = { numeric: "auto" }; function n(e) { e = { locale: (e = e || {}).locale || t, options: { ...o, ...e.options } }, this.rtf = new Intl.RelativeTimeFormat(e.locale, e.options) } return n.prototype = { from(t, o) { const n = t - (o || new Date); for (let t in e) if (Math.abs(n) > e[t] || "second" == t) return this.rtf.format(Math.round(n / e[t]), t) } }, n })); + +const relativeTime = new RelativeTime({ style: 'narrow' }); +// Global variables +const { html, render: renderElem } = uhtml; +const domRefs = {} +//Checks for internet connection status +if (!navigator.onLine) + notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', '', true) +window.addEventListener('offline', () => { + notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', true, true) +}) +window.addEventListener('online', () => { + getRef('notification_drawer').clearAll() + notify('We are back online.', 'success') +}) +// Use instead of document.getElementById +function getRef(elementId) { + if (!domRefs.hasOwnProperty(elementId)) { + domRefs[elementId] = { + count: 1, + ref: null, + }; + return document.getElementById(elementId); + } else { + if (domRefs[elementId].count < 3) { + domRefs[elementId].count = domRefs[elementId].count + 1; + return document.getElementById(elementId); + } else { + if (!domRefs[elementId].ref) + domRefs[elementId].ref = document.getElementById(elementId); + return domRefs[elementId].ref; + } + } +} + +// returns dom with specified element +function createElement(tagName, options = {}) { + const { className, textContent, innerHTML, attributes = {} } = options + const elem = document.createElement(tagName) + for (let attribute in attributes) { + elem.setAttribute(attribute, attributes[attribute]) + } + if (className) + elem.className = className + if (textContent) + elem.textContent = textContent + if (innerHTML) + elem.innerHTML = innerHTML + return elem +} + +// 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); + }; +} + +let zIndex = 50 +// function required for popups or modals to appear +function openPopup(popupId, pinned) { + zIndex++ + getRef(popupId).setAttribute('style', `z-index: ${zIndex}`) + getRef(popupId).show({ pinned }) + return getRef(popupId); +} + +// hides the popup or modal +function closePopup() { + if (popupStack.peek() === undefined) + return; + popupStack.peek().popup.hide() +} + + +// displays a popup for asking permission. Use this instead of JS confirm +const getConfirmation = (title, options = {}) => { + return new Promise(resolve => { + const { message = '', cancelText = 'Cancel', confirmText = 'OK' } = options + openPopup('confirmation_popup', true) + 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 + confirmButton.onclick = () => { + closePopup() + resolve(true); + } + cancelButton.onclick = () => { + closePopup() + resolve(false); + } + }) +} + +//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 }); +} + +// 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(' '); +} +window.addEventListener('hashchange', e => routeTo(window.location.hash)) +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') + DOMPurify.setConfig = { + FORBID_ATTR: ['style'], + FORBID_TAGS: ['style'] + + } + DOMPurify.addHook('afterSanitizeAttributes', function (node) { + // set all elements owning target to target=_blank + if ('target' in node) { + node.setAttribute('target', '_blank'); + } + // set non-HTML/MathML links to xlink:show=new + if ( + !node.hasAttribute('target') && + (node.hasAttribute('xlink:href') || node.hasAttribute('href')) + ) { + node.setAttribute('xlink:show', 'new'); + } + }); + document.querySelectorAll('sm-input[data-flo-id]').forEach(input => input.customValidation = floCrypto.validateAddr) + document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex) + document.addEventListener('keyup', (e) => { + if (e.code === 'Escape') { + closePopup() + } + }) + document.addEventListener("pointerdown", (e) => { + if (e.target.closest("button:not([disabled]), .interact")) { + createRipple(e, e.target.closest("button, .interact")); + } + }); + document.addEventListener('copy', () => { + notify('copied', 'success') + }) +}); + +function createRipple(event, target) { + const circle = document.createElement("span"); + const diameter = Math.max(target.clientWidth, target.clientHeight); + const radius = diameter / 2; + const targetDimensions = target.getBoundingClientRect(); + circle.style.width = circle.style.height = `${diameter}px`; + circle.style.left = `${event.clientX - (targetDimensions.left + radius)}px`; + circle.style.top = `${event.clientY - (targetDimensions.top + radius)}px`; + circle.classList.add("ripple"); + const rippleAnimation = circle.animate( + [ + { + transform: "scale(4)", + opacity: 0, + }, + ], + { + duration: floGlobals.prefersReducedMotion ? 0 : 600, + fill: "forwards", + easing: "ease-out", + } + ); + target.append(circle); + rippleAnimation.onfinish = () => { + circle.remove(); + }; +} + +function getFormattedTime(timestamp, format) { + try { + timestamp = parseInt(timestamp) + if (String(timestamp).length < 13) + timestamp *= 1000 + let [day, month, date, year] = new Date(timestamp).toString().split(' '), + minutes = new Date(timestamp).getMinutes(), + hours = new Date(timestamp).getHours(), + currentTime = new Date().toString().split(' ') + + minutes = minutes < 10 ? `0${minutes}` : minutes + let finalHours = ``; + if (hours > 12) + finalHours = `${hours - 12}:${minutes}` + else if (hours === 0) + finalHours = `12:${minutes}` + else + finalHours = `${hours}:${minutes}` + + finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM` + switch (format) { + case 'date-only': + return `${month} ${date}, ${year}`; + break; + case 'time-only': + return finalHours; + case 'relative': + return relativeTime.from(timestamp) + default: + return `${month} ${date}, ${year} at ${finalHours}`; + } + } catch (e) { + console.error(e); + return timestamp; + } +} + +const appState = { + params: {}, +} +const generalPages = ['sign_up', 'sign_in', 'loading', 'landing'] +function routeTo(targetPage, options = {}) { + const { firstLoad } = options + const routingAnimation = { in: slideInUp, out: slideOutUp } + let pageId + let subPageId1 + let searchParams + let params + if (targetPage === '') { + try { + if (floDapps.user.id) + pageId = 'dashboard_page' + } catch (e) { + pageId = 'landing' + } + } else { + if (targetPage.includes('/')) { + if (targetPage.includes('?')) { + const splitAddress = targetPage.split('?') + searchParams = splitAddress.pop(); + [, pageId, subPageId1] = splitAddress.pop().split('/') + } else { + [, pageId, subPageId1] = targetPage.split('/') + } + } else { + pageId = targetPage + } + } + + if (!document.querySelector(`#${pageId}`)?.classList.contains('inner-page')) return + try { + if (floDapps.user.id && (['sign_up', 'sign_in', 'loading', 'landing'].includes(pageId))) { + history.replaceState(null, null, '#/dashboard_page'); + pageId = 'dashboard_page' + } + } catch (e) { + if (!(generalPages.includes(pageId))) return + } + appState.currentPage = pageId + + if (searchParams) { + const urlSearchParams = new URLSearchParams('?' + searchParams); + params = Object.fromEntries(urlSearchParams.entries()); + } + if (params) + appState.params = params + if (firstLoad && floGlobals.tempUserTaskRequest && RIBC.getAllTasks()[floGlobals.tempUserTaskRequest]) { + requestForTask() + } + switch (pageId) { + case 'landing': + if (!params) { + params = { category: 'all' } + } + renderElem(getRef('landing_tasks_wrapper'), render.displayTasks(params.category, params.search)) + if (subPageId1) { + showTaskDetails(params.id) + } else { + hideTaskDetails() + } + break; + case 'sign_up': + const { floID, privKey } = floCrypto.generateNewID() + getRef('generated_flo_address').value = floID + getRef('generated_private_key').value = privKey + break; + case 'dashboard_page': + let renderedAssignedTasks + if (typeOfUser === 'intern') { + // Render assigned task cards + if (floGlobals.assignedTasks.size) { + renderedAssignedTasks = filterMap(floGlobals.assignedTasks, id => render.internTaskCard(id)) + } else { + renderedAssignedTasks = html`No task assigned yet.`; + } + } + renderElem(getRef('dashboard_page'), html` + + ${typeOfUser === 'intern' ? html`My tasks` : ''} + All tasks + Projects + ${floGlobals.isMobileView ? html`Leaderboard` : ''} + + ${typeOfUser === 'intern' ? html` +
    +
      ${renderedAssignedTasks}
    +
    + ` : ''} +
    ${render.displayTasks('all', params?.search)}
    + +
    +
    + +

    Leaderboard

    + All +
    +
    +
    +

    There are no interns

    +
    +
    + `) + render.dashProjects(getRef('pinned_projects'), pinnedProjects); + // displays recent projects + const unpinnedProjects = RIBC.getProjectList().filter(project => !pinnedProjects.includes(project)).reverse() + if (unpinnedProjects.length > 0) { + getRef('project_list_container').classList.remove('hidden') + } else { + getRef('project_list_container').classList.add('hidden') + } + render.dashProjects(getRef('project_list'), unpinnedProjects) + delegate(getRef('top_interns'), 'click', '.intern-card', e => { + showInternInfo(e.delegateTarget.dataset.internFloId) + }) + //creates cards for highest performing interns + //sort interns earned points + const highPerformingInterns = Object.keys(RIBC.getInternList()).sort((a, b) => { + return RIBC.getInternRating(b) - RIBC.getInternRating(a) + }); + renderElem(getRef('top_interns'), html`${highPerformingInterns.slice(0, 8).map(floId => render.internCard(floId))}`); + if (subPageId1) { + showTaskDetails(params.id) + } else { + hideTaskDetails() + } + break; + case 'updates_page': { + if (!getRef('updates_page__project_selector').children.length) { + renderProjectSelectorOptions() + renderInternSelectorOptions() + } + const { projectCode, internId, date } = params || getUpdateFilters() + if (params) { + setUpdateFilters({ projectCode, internId, date }) + } else if (projectCode) { + const dateParam = date !== '' ? `&date=${date}` : '' + history.replaceState(null, null, `#/updates_page?projectCode=${projectCode}&internId=${internId}${dateParam}`) + } + let matchedUpdates + if (projectCode !== 'all') { + matchedUpdates = getUpdatesByProject(projectCode) + } + if (internId !== 'all') { + matchedUpdates = getUpdatesByIntern(internId, matchedUpdates) + } + if (date) { + matchedUpdates = getUpdatesByDate(date, matchedUpdates) + } + renderInternUpdates(matchedUpdates) + } break; + case 'applications': + render.taskApplications() + if (subPageId1) { + showTaskDetails(params.id) + } else { + hideTaskDetails() + } + break; + case 'all_interns_page': + renderAllInterns() + break; + case 'project_explorer': + if (subPageId1) { + if (params) { + const { id: projectCode, branch } = params + if (appState.params.projectCode !== projectCode) { + showProjectInfo(projectCode) + const allProjects = getRef('project_explorer__left').querySelectorAll('.project-card'); + allProjects.forEach(project => project.classList.remove('project-card--active')) + const targetProject = [...allProjects].find(project => project.getAttribute('href').includes(projectCode)) + if (targetProject) + targetProject.classList.add('project-card--active') + } + if (branch) { + renderBranchTasks() + } + getRef('project_explorer__left').classList.add('hide-on-mobile') + getRef('project_explorer__right').classList.remove('hide-on-mobile') + } else { + getRef('project_explorer__left').querySelectorAll('.project-card').forEach(project => project.classList.remove('project-card--active')) + } + } else { + getRef('project_explorer__left').classList.remove('hide-on-mobile') + getRef('project_explorer__right').classList.add('hide-on-mobile') + history.replaceState(null, '', '#/project_explorer') + } + break; + case 'admin_page': + if (subPageId1) { + if (params && RIBC.getProjectList().includes(params.id)) { + const { id: projectCode, branch } = params + renderAdminProjectView(projectCode) + if (branch) { + renderBranchTasks() + } + getRef('projects_container__left').classList.add('hide-on-mobile') + getRef('project_editing_panel').classList.remove('hidden') + } + } else { + getRef('projects_container__left').classList.remove('hide-on-mobile') + getRef('project_editing_panel').classList.add('hidden') + history.replaceState(null, '', '#/admin_page') + } + break; + } + switch (appState.lastPage) { + case 'project_explorer': + case 'all_interns_page': + routingAnimation.in = slideInRight; + routingAnimation.out = slideOutRight; + break; + } + switch (pageId) { + case 'project_explorer': + case 'all_interns_page': + routingAnimation.in = slideInLeft; + routingAnimation.out = slideOutLeft; + break; + } + if (appState.lastPage !== pageId) { + if (document.querySelector('.nav-list__item--active')) + document.querySelector('.nav-list__item--active').classList.remove('nav-list__item--active'); + const targetListItem = [...document.querySelectorAll(`a.nav-list__item`)].find(item => item.href.includes(pageId)) + if (targetListItem) + targetListItem.classList.add('nav-list__item--active') + document.querySelectorAll('.page').forEach(page => page.classList.add('hidden')) + getRef(pageId).closest('.page').classList.remove('hidden') + let ogOverflow = getRef(pageId).parentNode.style.overflow + getRef(pageId).parentNode.style.overflow = 'hidden'; + if (appState.lastPage) { + getRef(appState.lastPage).animate(routingAnimation.out, { duration: floGlobals.prefersReducedMotion ? 0 : 300, fill: 'forwards', easing: 'ease' }).onfinish = (e) => { + e.target.effect.target.classList.add('hidden') + } + } + getRef(pageId).classList.remove('hidden') + getRef(pageId).animate(routingAnimation.in, { duration: floGlobals.prefersReducedMotion ? 0 : 300, fill: 'forwards', easing: 'ease' }).onfinish = (e) => { + getRef(pageId).parentNode.style.overflow = ogOverflow; + switch (pageId) { + case 'sign_in': + getRef('private_key_field').focusIn() + break; + } + } + appState.lastPage = pageId + } +} +// 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' ? getRef(id) : id; + button.disabled = show; + const animOptions = { + duration: floGlobals.prefersReducedMotion ? 0 : 200, + fill: 'forwards', + easing: 'ease' + } + if (show) { + button.animate([ + { + clipPath: 'circle(100%)', + }, + { + clipPath: 'circle(0)', + }, + ], animOptions).onfinish = e => { + e.target.commitStyles() + e.target.cancel() + } + button.parentNode.append(createElement('sm-spinner')) + } else { + button.style = '' + const potentialTarget = button.parentNode.querySelector('sm-spinner') + if (potentialTarget) potentialTarget.remove(); + } +} +// implement event delegation +function delegate(el, event, selector, fn) { + el.addEventListener(event, function (e) { + const potentialTarget = e.target.closest(selector) + if (potentialTarget) { + e.delegateTarget = potentialTarget + fn.call(this, e) + } + }) +} +const slideInLeft = [ + { + opacity: 0, + transform: 'translateX(1rem)' + }, + { + opacity: 1, + transform: 'translateX(0)' + } +] +const slideOutLeft = [ + { + opacity: 1, + transform: 'translateX(0)' + }, + { + opacity: 0, + transform: 'translateX(-1rem)' + }, +] +const slideInRight = [ + { + opacity: 0, + transform: 'translateX(-1rem)' + }, + { + opacity: 1, + transform: 'translateX(0)' + } +] +const slideOutRight = [ + { + opacity: 1, + transform: 'translateX(0)' + }, + { + opacity: 0, + transform: 'translateX(1rem)' + }, +] +const slideInDown = [ + { + opacity: 0, + transform: 'translateY(-1rem)' + }, + { + opacity: 1, + transform: 'translateY(0)' + }, +] +const slideOutDown = [ + { + opacity: 1, + transform: 'translateY(0)' + }, + { + opacity: 0, + transform: 'translateY(1rem)' + }, +] +const slideInUp = [ + { + opacity: 0, + transform: 'translateY(1rem)' + }, + { + opacity: 1, + transform: 'translateY(0)' + }, +] +const slideOutUp = [ + { + opacity: 1, + transform: 'translateY(0)' + }, + { + opacity: 0, + transform: 'translateY(-1rem)' + }, +] + +function showChildElement(id, index, options = {}) { + return new Promise((resolve) => { + const { mobileView = false, entry, exit } = options + const animOptions = { + duration: floGlobals.prefersReducedMotion ? 0 : 150, + easing: 'ease', + fill: 'forwards' + } + const parent = typeof id === 'string' ? document.getElementById(id) : id; + const visibleElement = [...parent.children].find(elem => !elem.classList.contains(mobileView ? 'hide-on-mobile' : 'hidden')); + if (visibleElement === parent.children[index]) return; + visibleElement.getAnimations().forEach(anim => anim.cancel()) + parent.children[index].getAnimations().forEach(anim => anim.cancel()) + if (visibleElement) { + if (exit) { + parent.style.overflow = 'hidden' + visibleElement.animate(exit, animOptions).onfinish = () => { + visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden') + parent.style.overflow = '' + } + parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden') + if (entry) + parent.children[index].animate(entry, animOptions).onfinish = () => resolve() + } else { + visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden') + parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden') + resolve() + } + } else { + parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden') + parent.children[index].animate(entry, animOptions).onfinish = () => resolve() + } + }) +} +function togglePrivateKeyVisibility(input) { + const target = input.closest('sm-input') + target.type = target.type === 'password' ? 'text' : 'password'; + target.focusIn() +} +function filterMap(array, mapFn) { + const result = []; + array.forEach((element, index) => { + const mapped = mapFn(element, index) + if (mapped) result.push(mapped) + }) + return result; +} +const mobileQuery = window.matchMedia('(max-width: 40rem)') +function handleMobileChange(e) { + floGlobals.isMobileView = e.matches +} +mobileQuery.addEventListener('change', handleMobileChange) +handleMobileChange(mobileQuery) +const reduceMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); +reduceMotionQuery.addEventListener('change', () => { + floGlobals.prefersReducedMotion = reduceMotionQuery.matches +}); +floGlobals.prefersReducedMotion = reduceMotionQuery.matches \ No newline at end of file