ribc/scripts/app_ui.js
2022-10-27 16:18:09 +05:30

1608 lines
87 KiB
JavaScript

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`
<li class=${`display-task`}>
<div class="flex align-center space-between">
<a class="display-task__category" href=${`#/landing?category=${category}`} title=${`See all ${floGlobals.taskCategories[category]} tasks`}>${floGlobals.taskCategories[category]}</a>
<a href=${`#/${appState.currentPage}/task?id=${projectCode}_${branch}_${task}`} class="display-task__link button button--small button--colored">
View details
<svg class="icon" style="margin-right: -0.5rem" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z"/></svg>
</a>
</div>
<h4 class="display-task__title">${title}</h4>
<div class="display-task__details flex flex-wrap gap-0-3">
${duration ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Duration: </span>
<span class="display-task__detail__value">${duration} ${durationType}</span>
</div>
`: ''}
${maxSlots ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Slots: </span>
<span class="display-task__detail__value">${maxSlots}</span>
</div>
`: ''}
${reward ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Reward: </span>
<span class="display-task__detail__value" style="color: var(--green)">₹${reward}</span>
</div>
`: ''}
</div>
</li>
`;
},
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`<p>No tasks related to <b>${searchQuery}</b></p>`
}
// render categories
let renderedCategories = []
if (availableCategories.size > 1) {
renderedCategories = [html`<strip-option value='all' ?selected=${category === 'all'}>All</strip-option>`];
availableCategories.forEach(categoryID => {
categories.push(html`<strip-option value=${categoryID} ?selected=${categoryID === category}>${floGlobals.taskCategories[categoryID]}</strip-option>`)
})
}
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`
<div id="display_task_search_wrapper" class="flex flex-direction-column gap-1">
<div class="flex align-center gap-1 flex-wrap space-between">
<h3>
Available Tasks
</h3>
${(filtered.length > 0 || searchQuery) ? html`
<sm-input id="task_search_input" oninput="filterTasks(event)" placeholder="Search keywords" type="search">
<svg slot="icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"> <circle cx="11" cy="11" r="8"></circle> <line x1="21" y1="21" x2="16.65" y2="16.65"></line> </svg>
</sm-input>
`: ''}
</div>
${availableCategories.size > 1 ? html`<strip-select id="task_category_selector" onchange='filterTasks()'>${renderedCategories}</strip-select>` : ''}
</div>
<div class="grid gap-1">
<ul id="display_task_list" class="flex flex-direction-column gap-0-5 observe-empty-state">${renderedTasks}</ul>
<div class="empty-state">
<p>Nothing to see here</p>
</div>
</div>
`;
},
projectCard(projectCode, isAdmin = false, ref) {
const projectName = RIBC.getProjectDetails(projectCode).projectName
const page = isAdmin ? 'admin_page' : 'project_explorer'
return html.for(ref, projectCode)`<a class="project-card flex align-center interact" title="Project information" href=${`#/${page}/project?id=${projectCode}&branch=mainLine`}>${projectName}</a>`
},
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`
<button class="button button--primary apply-button" .dataset=${{ taskId: `${appState.params.id}_${appState.params.branch}_${task}` }} ?disabled=${hasApplied}>
${hasApplied ? 'Applied' : 'Apply'}
</button>`;
}
const linkifyDescription = createElement('p', {
innerHTML: DOMPurify.sanitize(linkify(description)),
className: `timeline-task__description ws-pre-line wrap-around`
})
return html`
<div class=${`task ${status}`}>
<div class="left">
<div class="circle">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>
</div>
<div class="line"></div>
</div>
<div class="right">
<div class="apply-container flex space-between align-items-start">
<h4 class="timeline-task__title capitalize">${title}</h4>
${applyButton}
</div>
${assignedInternsCards.length ? html`<div class="assigned-interns">${assignedInternsCards}</div>` : ''}
<details>
<summary class="task-details__summary">Task details</summary>
${linkifyDescription}
</details>
<div class="timeline-task__details flex flex-wrap gap-0-3">
${duration ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Duration: </span>
<span class="display-task__detail__value">${duration} ${durationType}</span>
</div>
`: ''}
${maxSlots ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Slots: </span>
<span class="display-task__detail__value">${maxSlots}</span>
</div>
`: ''}
${reward ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Reward: </span>
<span class="display-task__detail__value" style="color: var(--green)">₹${reward}</span>
</div>
`: ''}
${branchesButtons.length ? html`<div class="task__branch_container">${branchesButtons}</div>` : ''}
</div>
</div>
`;
},
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`
<label class="intern-card align-center interact" .dataset=${{ internFloId }} title="Intern Information">
${selectable ? html`<input type="checkbox" class="intern-card__checkbox" value=${internFloId}>` : ''}
<div class="intern-card__initials" style=${`--color: var(${getInternColor(internFloId)})`}>${initials}</div>
<div class="intern-card__name capitalize">${internName}</div>
<div class="intern-card__score-wrapper flex align-center">
<h4 class="intern-card__score">${internPoints}</h4>
<svg class="icon icon--star" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <path fill="none" d="M0 0h24v24H0z" /> <path d="M12 18.26l-7.053 3.948 1.575-7.928L.587 8.792l8.027-.952L12 .5l3.386 7.34 8.027.952-5.935 5.488 1.575 7.928z" /> </svg>
</div>
</label>`;
},
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`<button class="button button--small init-update-replay margin-left-auto">Reply</button>`
}
let providedLink
if (link) {
providedLink = html`<a href=${link} target="_blank" rel="noopener noreferrer">${link}</a> `
}
let adminReply
if (note) {
adminReply = html`<div class="admin-reply grid">
<h4 class="admin-reply__title">Admin</h4>
<p class="admin-reply__description ws-pre-line wrap-around">${note}</p>
</div>`
}
return html.node`
<li class="intern-update" data-vector-clock="${`${time}_${floID}`}">
<div class="flex align-center space-between">
<span class="update__sender">${internName}</span>
<span class="update__time">${getFormattedTime(time)}</span>
</div>
<h4 class="update__topic">${topic}</h4>
<p class="update__message ws-pre-line wrap-around">${description}</p>
${providedLink}
${replyButton}
${adminReply}
</li>`;
},
branchButton(obj = {}) {
const { projectCode, branch, page, active = false } = obj
return html`
<a class=${`branch-button ${active ? 'branch-button--active' : ''}`} href=${`#/${page}/project?id=${projectCode}&branch=${branch}`}>
${branch}
</a>
`;
},
assignedInternCard(internFloId, options) {
let optionsButton
if (options) {
optionsButton = html` <button>
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
</button> `;
}
return html`
<span class="assigned-intern" data-flo-id="${internFloId}">
${RIBC.getInternList()[internFloId]}
${optionsButton}
</span>
`
},
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`<sm-option value=${categoryID} ?selected=${categoryID === category}>${floGlobals.taskCategories[categoryID]}</sm-option>`)
}
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}`)`
<li class="task-list-item" .dataset=${{ taskId: task }}>
<div class="flex align-center gap-0-3">
<div class="flex align-center gap-0-5">
<sm-checkbox ?checked=${status === 'completed'}>
<p class="margin-left-0-5">Mark as complete</p>
</sm-checkbox>
<div class="task-list-item__task-number">ID: ${task}</div>
</div>
<button class="button--danger icon-only margin-left-auto"onclick="removeThisTask()">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z"/></svg>
</button>
<button class="icon-only task-option">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <path fill="none" d="M0 0h24v24H0z" /> <path d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 14c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-7c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" /> </svg>
</button>
</div>
<h4 class="task-title capitalize" data-editable data-edit-field="title">${title}</h4>
<div class="assigned-interns">
<button class="button--outlined button--small button--colored" onclick="currentTask=this.closest('.task-list-item');openPopup('intern_list_popup')">
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0-6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zM5 18c.2-.63 2.57-1.68 4.96-1.94l2.04-2c-.39-.04-.68-.06-1-.06-2.67 0-8 1.34-8 4v2h9l-2-2H5zm15.6-5.5l-5.13 5.17-2.07-2.08L12 17l3.47 3.5L22 13.91z"/></svg>
Assign intern
</button>
${assignedInternsCards}
</div>
${taskDescription}
<div class="grid gap-0-5 margin-top-1" style="grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));">
<sm-select data-edit-field="category" label="Category: ">${categories}</sm-select>
<div class="flex flex-1">
<sm-input data-edit-field="duration" value="${duration}" class="flex-1" placeholder="Duration" type="number" style="--border-radius: 0.5rem 0 0 0.5rem; border-right: thin solid rgba(var(--text-color), 0.3);" animate="" aria-label="Duration" role="textbox">
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <g> <g> <path d="M15,1H9v2h6V1z M11,14h2V8h-2V14z M19.03,7.39l1.42-1.42c-0.43-0.51-0.9-0.99-1.41-1.41l-1.42,1.42 C16.07,4.74,14.12,4,12,4c-4.97,0-9,4.03-9,9s4.02,9,9,9s9-4.03,9-9C21,10.88,20.26,8.93,19.03,7.39z M12,20c-3.87,0-7-3.13-7-7 s3.13-7,7-7s7,3.13,7,7S15.87,20,12,20z"></path> </g> </g> </g> </svg>
</sm-input>
<sm-select data-edit-field="durationType" class="flex-shrink-0" style="--select-border-radius: 0 0.5rem 0.5rem 0;" role="listbox" align-select="right" value="days">
<sm-option value="days" ?selected=${durationType === "days"}>Days</sm-option>
<sm-option value="months" ?selected=${durationType === "months"}>Months</sm-option>
</sm-select>
</div>
<sm-input data-edit-field="maxSlots" value=${maxSlots} placeholder="Max slots available" type="number" animate="" aria-label="Max slots available" role="textbox"> </sm-input>
<sm-input data-edit-field="reward" value=${reward} type="number" placeholder="Reward" animate="" aria-label="Reward" role="textbox">
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <g> <path d="M13.66,7C13.1,5.82,11.9,5,10.5,5L6,5V3h12v2l-3.26,0c0.48,0.58,0.84,1.26,1.05,2L18,7v2l-2.02,0c-0.25,2.8-2.61,5-5.48,5 H9.77l6.73,7h-2.77L7,14v-2h3.5c1.76,0,3.22-1.3,3.46-3L6,9V7L13.66,7z"> </path> </g> </g> </svg>
</sm-input>
</div>
${branchesButtons.length ? html`<div class="task__branch_container">${branchesButtons}</div>` : ''}
</li>
`;
},
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`
<li class="request-card" .dataset=${{ vectorClock, type: 'task' }}>
<div class="display-task__category justify-self-start">${floGlobals.taskCategories[category]}</div>
<p class="request-card__description">
<b class="capitalize">${internName || name}</b> applied for
<b class="capitalize">${RIBC.getTaskDetails(projectCode, branch, task).title}</b>
</p>
${!internName ? html`
<div class="request-card__details grid gap-0-5 margin-top-1">
${brief ? html`
<div class="grid gap-0-3">
<h5>Educational background</h5>
<p class="ws-pre-line wrap-around">${brief}</p>
</div>
` : ''}
${contact ? html`
<div class="grid gap-0-3">
<h5>Contact</h5>
<sm-copy value=${contact}></sm-copy>
</div>
` : ''}
${portfolioLink ? html`
<div class="grid gap-0-3">
<h5>Portfolio link</h5>
<a href="${portfolioLink}" target="_blank">${portfolioLink}</a>
</div>
` : ''}
</div>
` : ''}
<div class="flex gap-0-3 margin-left-auto">
<button class="button button--small reject-request">
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
Reject
</button>
<button class="button button--small button--primary accept-request">
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>
Accept
</button>
</div>
</li>
`;
},
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`
<li class="task-card" data-unique-id="${uniqueId}">
<span class="task__project-title">${projectName}</span>
<div>
<h4 class="task__title">${title}</h4>
${linkifyDescription}
</div>
<button class="send-update-button button--small margin-left-auto" onclick=${initTaskUpdate}>
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M1.946 9.315c-.522-.174-.527-.455.01-.634l19.087-6.362c.529-.176.832.12.684.638l-5.454 19.086c-.15.529-.455.547-.679.045L12 14l6-8-8 6-8.054-2.685z"/></svg>
Post an update
</button>
</li>
`;
},
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`<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8 6.2V4H7V2H17V4H16V12L18 14V16H17.8L14 12.2V4H10V8.2L8 6.2ZM20 20.7L18.7 22L12.8 16.1V22H11.2V16H6V14L8 12V11.3L2 5.3L3.3 4L20 20.7ZM8.8 14H10.6L9.7 13.1L8.8 14Z"/> </svg>`;
} else {
pinIcon = html`<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M16 12V4H17V2H7V4H8V12L6 14V16H11.2V22H12.8V16H18V14L16 12ZM8.8 14L10 12.8V4H14V12.8L15.2 14H8.8Z"/> </svg>`;
}
return html.for(ref, projectCode)`
<div class="pinned-card" data-id=${projectCode}>
<div class="project-icon">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <path fill="none" d="M0 0h24v24H0z" /> <path d="M12.414 5H21a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7.414l2 2zM4 7v12h16V7H4z" /> </svg>
</div>
<div class="flex space-between align-items-start">
<a class="flex align-center" href=${`#/project_explorer/project?id=${projectCode}&branch=mainLine`}>
<h4 class="project__title">${projectName}</h4>
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6-6-6z"/></svg>
</a>
<button class="icon-only pin-project" title=${`${isPinned ? 'Unpin' : 'Pin'} this project`} onclick="pinProject(this)" data-pinned=${isPinned}>${pinIcon}</button>
</div>
<div class="progress-bar">
<div class="progress-value" style=${`width: ${completePercent}%`}></div>
</div>
<span class="project__complete-percent">${completePercent}% complete</span>
</div>
`
},
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`<sm-option value=${cat}>${floGlobals.taskCategories[cat]}</sm-option>`);
renderElem(getRef('filter_requests_by_category'), html`${[html`<sm-option value='all' selected>All</sm-option>`, ...categoryOptions]}`)
}
if (requestProjects.size) {
const projectOptions = [...requestProjects].map(project => html`<sm-option value=${project}>${RIBC.getProjectDetails(project).projectName}</sm-option>`);
renderElem(getRef('filter_requests_by_project'), html`${[html`<sm-option value='all' selected>All</sm-option>`, ...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`<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>`
} else if (status === 'Rejected') {
icon = html`<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z"/></svg>`
} else {
icon = html`<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>`
}
return html`
<li class=${`status-card ${status?.toLowerCase() || 'pending'}`}>
<time class="status-card__time capitalize">${getFormattedTime(timestamp, 'relative')}</time>
<p class="status-card__details">
You applied for <a class="capitalize" href=${`#/${appState.currentPage}/task?id=${taskId}`}>${RIBC.getTaskDetails(projectCode, branch, task).title}</a>
</p>
<div class="flex align-center status-card__status">
${icon}
<span>${status || 'Under review'}</span>
</div>
</li>
`;
},
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`
<div class="flex" style="position: sticky; top: 0; background-color: rgba(var(--foreground-color),1); padding: 1rem 0 0.5rem 0">
<button class="button icon-only align-self-start" onclick="history.back()" title="Go back">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none"></path> <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"> </path> </svg>
</button>
</div>
<div class="grid gap-1">
<h5 class="capitalize">${floGlobals.taskCategories[category]}</h5>
<h2 id="task_title">${title}</h2>
<div class="display-task__details flex flex-wrap gap-0-3">
${duration ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Duration: </span>
<span class="display-task__detail__value">${duration} ${durationType}</span>
</div>
`: ''}
${maxSlots ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Slots: </span>
<span class="display-task__detail__value">${maxSlots}</span>
</div>
`: ''}
${reward ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Reward: </span>
<span class="display-task__detail__value" style="color: var(--green)">₹${reward}</span>
</div>
`: ''}
</div>
${descriptionTag}
</div>
${!hasApplied ? html`
<button class="button button--primary" .dataset=${{ taskId }} onclick="requestForTask(this)">Apply Now</button>
`: ''}
`);
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`<p class="margin-bottom-0-5">
Branched off from <a href=${`#/${appState.currentPage}/project?id=${projectCode}&branch=${parentBranch}`}> ${parentBranch} </a>
</p>`)
}
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`<sm-option value=${categoryID} ?selected=${first}>${floGlobals.taskCategories[categoryID]}</sm-option>`)
first = false;
}
const placeholderTask = html.node`
<div class="temp-task grid gap-0-5">
<sm-form style="--gap: 0.5rem;">
<sm-input class="temp-task__title" placeholder="Title" animate required></sm-input>
<sm-textarea class="temp-task__description" placeholder="Description" rows="6" required></sm-textarea>
<div class="grid gap-0-5" style="grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));">
<sm-select class="temp-task__category" label="Category: ">${categories}</sm-select>
<sm-input class="temp-task__max-slots flex-1" placeholder="Max slots available" type="number" animate> </sm-input>
<div class="flex flex-1">
<sm-input class="temp-task__duration flex-1" placeholder="Duration" type="number" style="--border-radius: 0.5rem 0 0 0.5rem; border-right: thin solid rgba(var(--text-color), 0.3);" animate>
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> </g> <g> <g> <g> <path d="M15,1H9v2h6V1z M11,14h2V8h-2V14z M19.03,7.39l1.42-1.42c-0.43-0.51-0.9-0.99-1.41-1.41l-1.42,1.42 C16.07,4.74,14.12,4,12,4c-4.97,0-9,4.03-9,9s4.02,9,9,9s9-4.03,9-9C21,10.88,20.26,8.93,19.03,7.39z M12,20c-3.87,0-7-3.13-7-7 s3.13-7,7-7s7,3.13,7,7S15.87,20,12,20z" /> </g> </g> </g> </svg>
</sm-input>
<sm-select class="temp-task__duration-type flex-shrink-0"
style="--select-border-radius: 0 0.5rem 0.5rem 0;">
<sm-option value="days" selected>Days</sm-option>
<sm-option value="months">Months</sm-option>
</sm-select>
</div>
<sm-input class="temp-task__reward flex-1" type="number" placeholder="Reward" animate>
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <g> <path d="M13.66,7C13.1,5.82,11.9,5,10.5,5L6,5V3h12v2l-3.26,0c0.48,0.58,0.84,1.26,1.05,2L18,7v2l-2.02,0c-0.25,2.8-2.61,5-5.48,5 H9.77l6.73,7h-2.77L7,14v-2h3.5c1.76,0,3.22-1.3,3.46-3L6,9V7L13.66,7z"> </path> </g> </g> </svg>
</sm-input>
</div>
<div class="flex align-center gap-0-3 margin-top-1">
<button class="button cancel-task-button">
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <path fill="none" d="M0 0h24v24H0z" /> <path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" /> </svg>
Cancel
</button>
<button type="submit" class="button button--primary add-task-button">
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"> <path fill="none" d="M0 0h24v24H0z" /> <path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z" /> </svg>
Add
</button>
</div>
</sm-form>
</div>
`;
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`<sm-option value="all" selected>All</sm-option>`];
RIBC.getProjectList().reverse().forEach(project => {
options.push(html`<sm-option value="${project}">${RIBC.getProjectDetails(project).projectName}</sm-option>`);
})
renderElem(getRef('updates_page__project_selector'), html`${options}`)
}
function renderInternSelectorOptions() {
const options = [html`<sm-option value="all" selected>All</sm-option>`];
const allInterns = Object.entries(RIBC.getInternList()).sort((a, b) => a[1].toLowerCase().localeCompare(b[1].toLowerCase()));
allInterns.forEach(intern => {
options.push(html`<sm-option value="${intern[0]}">${intern[1]}</sm-option>`);
})
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`
<sm-form class="update-replay grid gap-0-5">
<sm-textarea placeholder="Enter your reply here" class="update-reply-textarea" rows="4" required></sm-textarea>
<div class="flex align-center gap-0-3 margin-left-auto">
<button class="update-replay__cancel button button--small" onclick="cancelUpdateReply(this.closest('.update-replay'))">Cancel</button>
<div class="multi-state-button">
<button class="update-replay__submit button button--small button--primary" onclick="submitUpdateReply(this.closest('.update-replay'))" type="submit">Submit</button>
</div>
</div>
</sm-form>
`)
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`
<div class="admin-reply grid">
<h4 class="admin-reply__title">Admin</h4>
<p class="admin-reply__description ws-pre-line wrap-around">${replyText}</p>
</div>`)
}).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, '<a href="$1" target="_blank">$1</a>');
//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<a href="http://$2" target="_blank">$2</a>');
//Change email addresses to mailto:: links.
replacePattern3 = /(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,6})/gim;
replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
return replacedText;
}