ribc/index.html
SaketAnand 231ceb1da0
Some checks failed
Workflow push to Dappbundle / Build (push) Has been cancelled
Update index.html
2025-09-03 20:03:52 +05:30

4397 lines
277 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>RanchiMall Internships</title>
<meta name="description" content="Web app for managing interns and projects">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/main.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Rubik:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
rel="stylesheet">
<script src="scripts/components.js" defer></script>
<script id="floGlobals">
/* Constants for FLO blockchain operations !!Make sure to add this at beginning!! */
const floGlobals = {
blockchain: "FLO",
adminID: "FMyRTrz9CG4TFNM6rCQgy3VQ5NF23bY2xD",
application: "InternManage",
rmIncorporationId: "FKNW5eCCp2SnJMJ6pLLpUCvk5hAage8Jtk",
ribcId: "FDaX363r1ooANA9A2erhehhigNTnidq3o4",
certificateIssuerAddress: "FFCpiaZi31TpbYw5q5VNk8qJMeDiTLgsrE"
}
</script>
<script src="scripts/lib.js" defer></script>
<script src="scripts/floCrypto.js" defer></script>
<script src="scripts/floBlockchainAPI.js" defer></script>
<script src="scripts/compactIDB.js" defer></script>
<script src="scripts/floCloudAPI.js" defer></script>
<script src="scripts/floDapps.js" defer></script>
<script src="scripts/btcOperator.js" defer></script>
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
integrity="sha512-/hVAZO5POxCKdZMSLefw30xEVwjm94PAV9ynjskGbIpBvHO9EBplEcdUlBdCKutpZsF+La8Ag4gNrG0gAOn3Ig=="
crossorigin="anonymous" referrerpolicy="no-referrer" defer></script>
<script src="scripts/ribc.js" defer></script>
<script id="onLoadStartUp">
function onLoadStartUp() {
if (!window.location.hash || window.location.hash === '#') {
window.location.hash = '#/dashboard_page'; // or '#/landing'
}
routeTo('loading')
document.body.classList.remove('hidden')
floDapps.setCustomPrivKeyInput(getSignedIn);
floDapps.setMidStartup(() =>
new Promise((resolve, reject) => {
RIBC.refreshObjectData()
.then(() => {
resolve()
setTimeout(() => {
if (!floGlobals.loaded && window.location.hash && window.location.hash !== '#') {
routeTo(window.location.hash);
}
}, 0);
}).catch((err) => {
reject(err)
})
fetchCertificateIssueTxs()
fetchPayments()
})
)
floDapps.launchStartUp().then(result => {
console.log(result)
if (!floCrypto.validateFloID(floDapps.user.id)) {
floGlobals.myBtcID = floDapps.user.id
const type = coinjs.addressDecode(floGlobals.myBtcID).type
if (type === 'standard') {
floGlobals.myFloID = btcOperator.convert.legacy2legacy(floGlobals.myBtcID, 0x23);
} else if (type === 'bech32') {
floGlobals.myFloID = btcOperator.convert.bech2legacy(floGlobals.myBtcID, 0x23);
} else {
notify(`Multisig address can't be used to sign in`, 'error');
return;
}
} else {
floGlobals.myFloID = floDapps.user.id
floGlobals.myBtcID = btcOperator.convert.legacy2bech(floDapps.user.id)
}
getRef('user_profile_id').textContent = floGlobals.myFloID
let showingFloID = true
// alternating between floID and btcID every 10 seconds
setInterval(() => {
getRef('user_profile_id').textContent = showingFloID ? floGlobals.myBtcID : floGlobals.myFloID
showingFloID = !showingFloID
}, 10000)
floGlobals.isSubAdmin = floGlobals.subAdmins.includes(floGlobals.myFloID)
floGlobals.loaded = true
RIBC.init(floGlobals.isSubAdmin).then(result => {
console.log(result)
renderAllElements()
if (!window.location.hash) window.location.hash = '#/dashboard_page'; // or '#/landing'
routeTo(window.location.hash, { firstLoad: true })
}).catch(error => console.error(error))
}).catch(error => console.error(error))
}
</script>
</head>
<body onload="onLoadStartUp()" class="hidden">
<sm-notifications id="notification_drawer"></sm-notifications>
<sm-popup id="confirmation_popup">
<h4 id="confirm_title"></h4>
<p id="confirm_message"></p>
<div class="flex align-center gap-0-5 margin-left-auto">
<button class="button cancel-button">Cancel</button>
<button class="button button--primary confirm-button">OK</button>
</div>
</sm-popup>
<div id="secondary_pages" class="page">
<header class="flex align-center gap-1 space-between">
<div class="app-brand">
<svg id="main_logo" class="icon" viewBox="0 0 27.25 32">
<title>RanchiMall</title>
<path
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
</svg>
<div class="app-name">
<div class="app-name__company">RanchiMall</div>
<h4 class="app-name__title">
Internships
</h4>
</div>
</div>
<theme-toggle></theme-toggle>
</header>
<div id="landing" class="grid inner-page hidden">
<div class="gap-1-5 landing__card">
<div class="grid gap-1-5">
<h1>
Blockchain Internships
</h1>
<div class="flex gap-0-3">
<a href="#/sign_up" class="button"
style="background-color: rgba( 0 0 0 / 0.3); padding: 0.8rem;">Get
started</a>
<a href="#/sign_in" class="button button--primary">Sign in</a>
</div>
</div>
<img src="assets/working-intern.svg" alt="">
</div>
<div id="landing_tasks_wrapper" class="flex flex-direction-column justify-content-center"></div>
</div>
<article id="sign_in" class="inner-page hidden">
<section>
<h1 style="font-size: 2rem;">Sign in</h1>
<p>Welcome back, glad to see you again</p>
<sm-form id="sign_in_form">
<sm-input id="private_key_field" class="password-field" type="password"
placeholder="FLO private key" error-text="Private key is invalid" data-private-key required>
<label slot="right" class="interactive">
<input type="checkbox" class="hidden" autocomplete="off" readonly
onchange="togglePrivateKeyVisibility(this)">
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<title>Hide password</title>
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
<path
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
</svg>
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<title>Show password</title>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg>
</label>
</sm-input>
<button id="sign_in_button" class="button button--primary" type="submit" disabled>Sign in</button>
</sm-form>
<p>
New here? <a href="#/sign_up">get your FLO login credentials</a>
</p>
</section>
</article>
<article id="sign_up" class="inner-page hidden">
<keys-generator id="keys_generator"></keys-generator>
</article>
<div id="loading" class="inner-page hidden">
<cube-loader></cube-loader>
<h4 class="page__tag-line margin-block-1">Getting everything ready, hang on.</h4>
<button class="button" onclick="floDapps.clearCredentials()">Reset</button>
</div>
</div>
<div id="task_details" class="hidden">
<div id="task_details__backdrop" onclick="hideTaskDetails()"></div>
<div id="task_details_wrapper" class="flex flex-direction-column gap-1-5"></div>
</div>
<main id="main_page" class="grid page hidden">
<header id="main_header" class="space-between">
<div class="app-brand hide-on-mobile">
<svg id="main_logo" class="icon" viewBox="0 0 27.25 32">
<title>RanchiMall</title>
<path
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
</svg>
<div class="app-name">
<div class="app-name__company">RanchiMall</div>
<h4 class="app-name__title">
Internships
</h4>
</div>
</div>
<theme-toggle></theme-toggle>
<button id="user_profile_button" class="user-content button--small" onclick="openPopup('profile_popup')">
<svg xmlns="http://www.w3.org/2000/svg" class="icon margin-right-0-5" height="24px" viewBox="0 0 24 24"
width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none"></path>
<path
d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z">
</path>
</svg>
<div id="user_profile_id" class="overflow-ellipsis"></div>
</button>
<div id="commit_wrapper" class="flex align-center space-between gap-1 hidden">
<strong>Don't forget to save changes!</strong>
<button id="commit_changes_button" class="button button--small button--primary admin-option"
onclick="commitToChanges()">
<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="M7 19v-6h10v6h2V7.828L16.172 5H5v14h2zM4 3h13l4 4v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm5 12v4h6v-4H9z" />
</svg>
Save
</button>
</div>
</header>
<nav id="main_nav">
<a id="dashboard_btn" href="#/dashboard_page" class="nav-list__item nav-list__item--active interactive"
title="open dashboard page">
<svg class="icon icon--outlined" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M13 21V11h8v10h-8zM3 13V3h8v10H3zm6-2V5H5v6h4zM3 21v-6h8v6H3zm2-2h4v-2H5v2zm10 0h4v-6h-4v6zM13 3h8v6h-8V3zm2 2v2h4V5h-4z" />
</svg>
<svg class="icon icon--filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" />
</svg>
<span class="nav-list__item_title">
Dashboard
</span>
</a>
<a id="update_panel_btn" href="#/updates_page" class="nav-list__item interactive" title="show updates">
<svg class="icon icon--outlined" 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 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z" />
</svg>
<svg class="icon icon--filled" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
width="24px" fill="#000000">
<path
d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
</svg>
<span class="nav-list__item_title">
Updates
</span>
</a>
<a href="#/applications" class="nav-list__item interactive not-for-admin"
title="See status of applications">
<svg class="icon icon--outlined" 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>
<path d="M0,0h24v24H0V0z" fill="none" />
</g>
<g>
<g>
<path
d="M15,3H5C3.9,3,3.01,3.9,3.01,5L3,19c0,1.1,0.89,2,1.99,2H19c1.1,0,2-0.9,2-2V9L15,3z M5,19V5h9v5h5v9H5z M9,8 c0,0.55-0.45,1-1,1S7,8.55,7,8s0.45-1,1-1S9,7.45,9,8z M9,12c0,0.55-0.45,1-1,1s-1-0.45-1-1s0.45-1,1-1S9,11.45,9,12z M9,16 c0,0.55-0.45,1-1,1s-1-0.45-1-1s0.45-1,1-1S9,15.45,9,16z" />
</g>
</g>
</svg>
<svg class="icon icon--filled" 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>
<path d="M0,0h24v24H0V0z" fill="none" />
</g>
<g>
<g>
<path
d="M15,3H5C3.9,3,3.01,3.9,3.01,5L3,19c0,1.1,0.89,2,1.99,2H19c1.1,0,2-0.9,2-2V9L15,3z M8,17c-0.55,0-1-0.45-1-1s0.45-1,1-1 s1,0.45,1,1S8.55,17,8,17z M8,13c-0.55,0-1-0.45-1-1s0.45-1,1-1s1,0.45,1,1S8.55,13,8,13z M8,9C7.45,9,7,8.55,7,8s0.45-1,1-1 s1,0.45,1,1S8.55,9,8,9z M14,10V4.5l5.5,5.5H14z" />
</g>
</g>
</svg>
<span class="nav-list__item_title">
Applications
</span>
</a>
<a id="admin_page_nav_button" href="#/admin_page"
class="admin-option nav-list__item interactive open-first-project" title="open admin panel">
<svg class="icon icon--outlined" 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>
<path d="M0,0h24v24H0V0z" fill="none" />
</g>
<g>
<g>
<path
d="M4,18v-0.65c0-0.34,0.16-0.66,0.41-0.81C6.1,15.53,8.03,15,10,15c0.03,0,0.05,0,0.08,0.01c0.1-0.7,0.3-1.37,0.59-1.98 C10.45,13.01,10.23,13,10,13c-2.42,0-4.68,0.67-6.61,1.82C2.51,15.34,2,16.32,2,17.35V20h9.26c-0.42-0.6-0.75-1.28-0.97-2H4z" />
<path
d="M10,12c2.21,0,4-1.79,4-4s-1.79-4-4-4C7.79,4,6,5.79,6,8S7.79,12,10,12z M10,6c1.1,0,2,0.9,2,2s-0.9,2-2,2 c-1.1,0-2-0.9-2-2S8.9,6,10,6z" />
<path
d="M20.75,16c0-0.22-0.03-0.42-0.06-0.63l1.14-1.01l-1-1.73l-1.45,0.49c-0.32-0.27-0.68-0.48-1.08-0.63L18,11h-2l-0.3,1.49 c-0.4,0.15-0.76,0.36-1.08,0.63l-1.45-0.49l-1,1.73l1.14,1.01c-0.03,0.21-0.06,0.41-0.06,0.63s0.03,0.42,0.06,0.63l-1.14,1.01 l1,1.73l1.45-0.49c0.32,0.27,0.68,0.48,1.08,0.63L16,21h2l0.3-1.49c0.4-0.15,0.76-0.36,1.08-0.63l1.45,0.49l1-1.73l-1.14-1.01 C20.72,16.42,20.75,16.22,20.75,16z M17,18c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S18.1,18,17,18z" />
</g>
</g>
</svg>
<svg class="icon icon--filled" 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>
<path d="M0,0h24v24H0V0z" fill="none" />
</g>
<g>
<g>
<circle cx="10" cy="8" r="4" />
<path
d="M10.67,13.02C10.45,13.01,10.23,13,10,13c-2.42,0-4.68,0.67-6.61,1.82C2.51,15.34,2,16.32,2,17.35V20h9.26 C10.47,18.87,10,17.49,10,16C10,14.93,10.25,13.93,10.67,13.02z" />
<path
d="M20.75,16c0-0.22-0.03-0.42-0.06-0.63l1.14-1.01l-1-1.73l-1.45,0.49c-0.32-0.27-0.68-0.48-1.08-0.63L18,11h-2l-0.3,1.49 c-0.4,0.15-0.76,0.36-1.08,0.63l-1.45-0.49l-1,1.73l1.14,1.01c-0.03,0.21-0.06,0.41-0.06,0.63s0.03,0.42,0.06,0.63l-1.14,1.01 l1,1.73l1.45-0.49c0.32,0.27,0.68,0.48,1.08,0.63L16,21h2l0.3-1.49c0.4-0.15,0.76-0.36,1.08-0.63l1.45,0.49l1-1.73l-1.14-1.01 C20.72,16.42,20.75,16.22,20.75,16z M17,18c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S18.1,18,17,18z" />
</g>
</g>
</svg>
<span class="nav-list__item_title">
Manage
</span>
</a>
</nav>
<article id="sub_page_container">
<section id="dashboard_page" class="inner-page hidden"></section>
<section id="admin_page" class="inner-page hidden">
<div id="admin_page__header" class="flex align-center space-between">
<sm-chips id="admin_view_selector">
<sm-chip value="projects" selected>Projects</sm-chip>
<sm-chip value="interns">Interns</sm-chip>
<sm-chip value="task_display">Task display</sm-chip>
<sm-chip value="requests">Requests</sm-chip>
<sm-chip value="certificates">Certificates</sm-chip>
</sm-chips>
</div>
<div id="admin_views">
<section id="projects_container">
<div id="projects_container__left" class="flex flex-direction-column">
<button class="button button--colored justify-content-start margin-block-0-5"
onclick="openPopup('add_project_popup')">
<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>
<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 5v14h16V7h-8.414l-2-2H4zm7 7V9h2v3h3v2h-3v3h-2v-3H8v-2h3z">
</path>
</svg>
Add project
</button>
<div id="admin_page__project_list" class="list-container observe-empty-state"></div>
<h4 class="empty-state">No project added</h4>
</div>
<section id="project_editing_panel" class="hidden">
<div id="project_details_wrapper"
class="flex flex-direction-column gap-1 margin-bottom-2 align-items-start">
<a class="flex gap-0-3 align-center button--colored hide-on-desktop"
href="#/admin_page/projects" title="Go back">
<svg class="icon" style="margin-left: -0.3rem;" 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="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z" />
</svg>
Projects
</a>
<div class="flex flex-wrap align-items-start gap-0-3">
<h2 id="editing_panel__title" data-editable></h2>
<button class="button button--colored icon-only admin-option"
title="Edit this title" onclick="makeEditable(this.previousElementSibling)">
<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="M14.06 9.02l.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z" />
</svg>
</button>
</div>
<div class="flex flex-wrap align-items-start gap-0-3">
<p id="editing_panel__description" class="ws-pre-line wrap-around" data-editable>
</p>
<button class="button button--colored icon-only admin-option"
title="Edit this description"
onclick="makeEditable(this.previousElementSibling)">
<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="M14.06 9.02l.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z" />
</svg>
</button>
</div>
</div>
<div id="branch_container"></div>
<h4>Tasks</h4>
<ul id="task_list" class="grid observe-empty-state"></ul>
<div class="empty-state padding-block-1">
<p>
No tasks added yet, tasks will appear here after adding them.
</p>
</div>
<button id="add_task" class="button " onclick="addPlaceholderTask()">
<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="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z" />
</svg>
Add task
</button>
</section>
</section>
<section id="interns_container" class="flex flex-direction-column hidden">
<ul id="admin_page__intern_list" class="grid observe-empty-state"></ul>
<h4 class="empty-state">No interns added</h4>
</section>
<section id="task_display_container" class="flex hidden">
<div class="flex flex-direction-column gap-1 flex-1">
<h4>Displayed tasks</h4>
<ul id="display_task_map" class="drop-zone"></ul>
</div>
<div class="flex flex-direction-column gap-1 flex-1">
<h4>Available tasks</h4>
<ul id="all_tasks" class="drop-zone"></ul>
</div>
</section>
<section id="requests_container" class="grid hidden">
<div id="requests_container__filters" class="flex flex-direction-column gap-1">
<h5>Filters</h5>
<div class="grid gap-0-3">
<p>Category</p>
<sm-select id="filter_requests_by_category" onchange="render.internRequests()">
</sm-select>
</div>
<div class="grid gap-0-3">
<p>Project</p>
<sm-select id="filter_requests_by_project" onchange="render.internRequests()">
</sm-select>
</div>
<button class="button button--colored" onclick="clearRequestFilters()">Clear</button>
</div>
<div class="flex flex-direction-column gap-1">
<h5>Pending</h5>
<ul id="requests_list" class="list-container observe-empty-state"></ul>
<div class="empty-state">
<p>No pending requests</p>
</div>
</div>
</section>
<section id="certificates_container" class="hidden">
<div class="flex flex-direction-column gap-1">
<button class="button button--primary margin-right-auto gap-0-3"
onclick="openPopup('issue_certificate_popup')">
<svg 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>
<path
d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M7,9l1.41,1.41L11,7.83V16h2V7.83l2.59,2.58L17,9l-5-5L7,9z" />
</g>
</svg>
Issue a certificate
</button>
<ul id="certificates_list" class="list-container observe-empty-state"></ul>
<div class="empty-state">
<p>No certificates added</p>
</div>
</div>
</section>
</div>
</section>
<section id="updates_page" class="inner-page hidden">
<button class="button hide-on-desktop justify-self-end" onclick="toggleUpdatesFilter()">Filter</button>
<section id="update_filters_wrapper" class="grid hide-on-mobile">
<h4>Filter</h4>
<div class="grid gap-0-5">
<h5 class="uppercase">By Projects</h5>
<sm-select id="updates_page__project_selector"></sm-select>
</div>
<div class="grid gap-0-5">
<h5 class="uppercase">By Intern</h5>
<sm-select id="updates_page__intern_selector"></sm-select>
</div>
<label class="grid gap-0-5">
<h5 class="uppercase">By date</h5>
<input type="date" id="updates_page__date_selector" aria-label="Filter updates by date"
min="2020-01-01">
</label>
<button class="button" onclick="clearUpdatesFilter()">Clear</button>
</section>
<section id="updates_wrapper">
<ul id="all_updates_list" class="grid gap-0-5 observe-empty-state"></ul>
<h4 class="empty-state">No related updates</h4>
</section>
</section>
<section id="applications" class="inner-page hidden align-content-start">
<div class="grid gap-0-3">
<h3>Task applications</h3>
<p>Check status of your applications</p>
</div>
<ul id="task_requests_list" class="grid gap-0-5 observe-empty-state margin-top-1"></ul>
<h4 class="empty-state">No task requests</h4>
</section>
<section id="intern_profile" class="inner-page hidden flex align-start"></section>
<section id="all_interns_page" class="inner-page hidden flex flex-direction-column align-start">
<div id="all_interns_page__header" class="grid gap-0-5">
<h2>Interns</h2>
<sm-input id="interns_page__search" placeholder="Search">
<svg class="icon" slot="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="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z" />
</svg>
</sm-input>
</div>
<ul id="all_interns_list" class="grid observe-empty-state"></ul>
<h4 class="empty-state">No intern found</h4>
</section>
<section id="project_explorer" class="inner-page hidden">
<div id="project_explorer__breadcrumbs" class="flex flex-wrap align-center full-bleed"></div>
<div id="project_explorer__left" class="list-container">
<div id="all_projects"></div>
</div>
<section id="project_explorer__right" class="grid hidden">
<header class="flex flex-direction-column gap-0-5 align-items-start">
<h2 id="project_explorer__project_title"></h2>
</header>
<p id="project_explorer__project_description" class="ws-pre-line wrap-around"></p>
<a href="" id="project_explorer__project_updates"
class="button button--small margin-right-auto">Check
related updates</a>
<div id="explorer_branch_container" class="flex align-center flex-wrap gap-0-3 margin-top-0-5">
</div>
<div id="explorer_task_list" class="observe-empty-state margin-top-1"></div>
<h4 class="empty-state">No tasks are added to this projects</h4>
</section>
</section>
</article>
</main>
</main>
<sm-popup id="intern_list_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="closePopup()">
<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>
<h3 class="medium-block-0-5">Select interns</h3>
<button id="assign_interns_button" class="button button--primary" onclick="assignSelectedInterns()"
disabled>Assign</button>
</header>
<sm-input id="intern_search_field" placeholder="Search for interns" type="search" autofocus>
<svg slot="icon" 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="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z" />
</svg>
</sm-input>
<div id="intern_list_container" class="observe-empty-state"></div>
<h4 class="empty-state">No intern found</h4>
</sm-popup>
<sm-popup id="add_project_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="closePopup()">
<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>
<h3>Add new project</h3>
</header>
<sm-form>
<sm-input id="project_name_field" placeholder="Name" autofocus required></sm-input>
<sm-textarea id="project_description_field" placeholder="Description" rows="4" required></sm-textarea>
<button class="button button--primary" type="submit" onclick="addProjectToList()" disabled>
<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="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z" />
</svg>
Add
</button>
</sm-form>
</sm-popup>
<sm-popup id="add_intern_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="closePopup()">
<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>
<h3>Add new intern</h3>
</header>
<sm-form>
<sm-input id="intern_flo_id_field" placeholder="FLO address" error-text="Invalid FLO address" data-flo-id
required autofocus>
</sm-input>
<sm-input id="intern_name_field" placeholder="Name" required></sm-input>
<button class="button button--primary" type="submit" onclick="addInternToList()" disabled>
<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="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z" />
</svg>
Add
</button>
</sm-form>
</sm-popup>
<sm-popup id="task_editing_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="closePopup()">
<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>
<h3>Edit task</h3>
</header>
<sm-form id="task_edit_form">
<div class="grid gap-0-5">
<b>Title</b>
<sm-input id="edit_task_title" required></sm-input>
</div>
<div class="grid gap-0-5">
<b>Description</b>
<p id="edit_task_description" class="task-description ws-pre-line wrap-around" contenteditable="true">
</p>
</div>
<div class="grid gap-0-5 margin-top-1" style="grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));">
<sm-select id="edit_task_category" label="Category: "></sm-select>
<div class="flex flex-1">
<sm-input id="edit_task_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 id="edit_task_duration_type" class="flex-shrink-0"
style="--select-border-radius: 0 0.5rem 0.5rem 0;" role="listbox" align-select="right">
<sm-option value="days">Days</sm-option>
<sm-option value="months">Months</sm-option>
</sm-select>
</div>
<sm-input id="edit_task_max_slots" placeholder="Max slots available" type="number" animate=""
aria-label="Max slots available" role="textbox"> </sm-input>
<sm-input id="edit_task_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>
<button class="button button--primary" onclick="saveTaskChanges()" type="submit">Save</button>
</sm-form>
</sm-popup>
<sm-popup id="create_branch_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="closePopup()">
<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>
<h3>Create a new branch</h3>
</header>
<sm-form>
<sm-input id="branch_start_point" placeholder="Start point ID" type="number" required></sm-input>
<sm-input id="branch_merge_point" placeholder="Merge point ID" type="number"></sm-input>
<button id="create_branch_btn" class="button button--primary" type="submit">
<svg class="icon margin-right-0-5" 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>
Create
</button>
</sm-form>
</sm-popup>
<sm-popup id="post_update_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="closePopup()">
<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>
<h3>Post an update</h3>
</header>
<h5 id="update_of_project"></h5>
<h3 id="update_of_task"></h3>
<sm-form>
<sm-textarea id="update__brief" placeholder="Type the update" rows="4" autofocus required>
</sm-textarea>
<div class="multi-state-button">
<button id="post_update_btn" title="post this update" class="button button--primary" type="submit"
disabled onclick="postUpdate()">
<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>
<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">
</path>
</svg>
Post update
</button>
</div>
</sm-form>
</sm-popup>
<sm-popup id="apply_for_task_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="closePopup()">
<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>
<h3>Apply for task</h3>
</header>
<sm-form>
<div class="grid">
<h5>Applying for</h5>
<h3 id="intern_apply__task"></h3>
</div>
<sm-input id="intern_apply__name" placeholder="Full name" autofocus required animate></sm-input>
<sm-input id="intern_apply__contact" placeholder="WhatsApp number" required animate></sm-input>
<sm-textarea id="intern_apply__brief" placeholder="Educational background" rows="4" required></sm-textarea>
<sm-input id="intern_apply__portfolio_link" placeholder="Portfolio link (optional)" animate>
</sm-input>
<div class="multi-state-button">
<button id="intern_apply__button" title="post this update" class="button button--primary w-100"
type="submit" disabled onclick="applyForInternship()">
<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.923 9.37c-.51-.205-.504-.51.034-.689l19.086-6.362c.529-.176.832.12.684.638l-5.454 19.086c-.15.529-.475.553-.717.07L11 13 1.923 9.37zm4.89-.2l5.636 2.255 3.04 6.082 3.546-12.41L6.812 9.17z" />
</svg>
Apply
</button>
</div>
</sm-form>
</sm-popup>
<sm-popup id="rate_participants_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close justify-self-start" onclick="closePopup()">
<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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
</button>
<h3>Rate interns</h3>
</header>
<div id="rating_wrapper" class="flex flex-direction-column gap-1-5"></div>
</sm-popup>
<sm-popup id="secure_pwd_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close justify-self-start" onclick="closePopup()">
<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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
</button>
<h3 id="secure_pwd_title">Set password</h3>
</header>
<sm-form>
<sm-input id="secure_pwd_input" class="password-field" type="password" placeholder="Password" animate
required autofocus>
<label slot="right" class="interactive">
<input type="checkbox" class="hidden" autocomplete="off" readonly
onchange="togglePrivateKeyVisibility(this)">
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
width="24px" fill="#000000">
<title>Hide password</title>
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
<path
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
</svg>
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
width="24px" fill="#000000">
<title>Show password</title>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg>
</label>
</sm-input>
<button class="button button--primary cta secure-priv-key" type="submit" onclick="setSecurePassword()">
Set
</button>
</sm-form>
</sm-popup>
<sm-popup id="issue_certificate_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close justify-self-start" onclick="closePopup()">
<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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
</button>
<h3>Issue certificate</h3>
</header>
<div id="issue_certificate_popup__content"></div>
</sm-popup>
<sm-popup id="profile_popup">
<header slot="header" class="popup__header">
<button class="popup__header__close justify-self-start" onclick="closePopup()">
<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="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
</button>
<h3>Profile</h3>
</header>
<div id="profile_popup__content" class="grid gap-3"></div>
</sm-popup>
<script>
// dragula
!function (e) { "object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define([], e) : ("undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : this).dragula = e() }(function () { return function o(r, i, u) { function c(t, e) { if (!i[t]) { if (!r[t]) { var n = "function" == typeof require && require; if (!e && n) return n(t, !0); if (a) return a(t, !0); throw (n = new Error("Cannot find module '" + t + "'")).code = "MODULE_NOT_FOUND", n } n = i[t] = { exports: {} }, r[t][0].call(n.exports, function (e) { return c(r[t][1][e] || e) }, n, n.exports, o, r, i, u) } return i[t].exports } for (var a = "function" == typeof require && require, e = 0; e < u.length; e++)c(u[e]); return c }({ 1: [function (e, t, n) { "use strict"; var o = {}, r = "(?:^|\\s)", i = "(?:\\s|$)"; function u(e) { var t = o[e]; return t ? t.lastIndex = 0 : o[e] = t = new RegExp(r + e + i, "g"), t } t.exports = { add: function (e, t) { var n = e.className; n.length ? u(t).test(n) || (e.className += " " + t) : e.className = t }, rm: function (e, t) { e.className = e.className.replace(u(t), " ").trim() } } }, {}], 2: [function (e, t, n) { (function (r) { "use strict"; var M = e("contra/emitter"), k = e("crossvent"), j = e("./classes"), R = document, q = R.documentElement; function U(e, t, n, o) { r.navigator.pointerEnabled ? k[t](e, { mouseup: "pointerup", mousedown: "pointerdown", mousemove: "pointermove" }[n], o) : r.navigator.msPointerEnabled ? k[t](e, { mouseup: "MSPointerUp", mousedown: "MSPointerDown", mousemove: "MSPointerMove" }[n], o) : (k[t](e, { mouseup: "touchend", mousedown: "touchstart", mousemove: "touchmove" }[n], o), k[t](e, n, o)) } function K(e) { if (void 0 !== e.touches) return e.touches.length; if (void 0 !== e.which && 0 !== e.which) return e.which; if (void 0 !== e.buttons) return e.buttons; e = e.button; return void 0 !== e ? 1 & e ? 1 : 2 & e ? 3 : 4 & e ? 2 : 0 : void 0 } function z(e, t) { return void 0 !== r[t] ? r[t] : (q.clientHeight ? q : R.body)[e] } function H(e, t, n) { var o = (e = e || {}).className || ""; return e.className += " gu-hide", n = R.elementFromPoint(t, n), e.className = o, n } function V() { return !1 } function $() { return !0 } function G(e) { return e.width || e.right - e.left } function J(e) { return e.height || e.bottom - e.top } function Q(e) { return e.parentNode === R ? null : e.parentNode } function W(e) { return "INPUT" === e.tagName || "TEXTAREA" === e.tagName || "SELECT" === e.tagName || function e(t) { if (!t) return !1; if ("false" === t.contentEditable) return !1; if ("true" === t.contentEditable) return !0; return e(Q(t)) }(e) } function Z(t) { return t.nextElementSibling || function () { var e = t; for (; e = e.nextSibling, e && 1 !== e.nodeType;); return e }() } function ee(e, t) { var t = (n = t).targetTouches && n.targetTouches.length ? n.targetTouches[0] : n.changedTouches && n.changedTouches.length ? n.changedTouches[0] : n, n = { pageX: "clientX", pageY: "clientY" }; return e in n && !(e in t) && n[e] in t && (e = n[e]), t[e] } t.exports = function (e, t) { var l, f, s, d, m, o, r, v, p, h, n; 1 === arguments.length && !1 === Array.isArray(e) && (t = e, e = []); var i, g = null, y = t || {}; void 0 === y.moves && (y.moves = $), void 0 === y.accepts && (y.accepts = $), void 0 === y.invalid && (y.invalid = function () { return !1 }), void 0 === y.containers && (y.containers = e || []), void 0 === y.isContainer && (y.isContainer = V), void 0 === y.copy && (y.copy = !1), void 0 === y.copySortSource && (y.copySortSource = !1), void 0 === y.revertOnSpill && (y.revertOnSpill = !1), void 0 === y.removeOnSpill && (y.removeOnSpill = !1), void 0 === y.direction && (y.direction = "vertical"), void 0 === y.ignoreInputTextSelection && (y.ignoreInputTextSelection = !0), void 0 === y.mirrorContainer && (y.mirrorContainer = R.body); var w = M({ containers: y.containers, start: function (e) { e = S(e); e && C(e) }, end: O, cancel: L, remove: X, destroy: function () { c(!0), N({}) }, canMove: function (e) { return !!S(e) }, dragging: !1 }); return !0 === y.removeOnSpill && w.on("over", function (e) { j.rm(e, "gu-hide") }).on("out", function (e) { w.dragging && j.add(e, "gu-hide") }), c(), w; function u(e) { return -1 !== w.containers.indexOf(e) || y.isContainer(e) } function c(e) { e = e ? "remove" : "add"; U(q, e, "mousedown", E), U(q, e, "mouseup", N) } function a(e) { U(q, e ? "remove" : "add", "mousemove", x) } function b(e) { e = e ? "remove" : "add"; k[e](q, "selectstart", T), k[e](q, "click", T) } function T(e) { i && e.preventDefault() } function E(e) { var t, n; o = e.clientX, r = e.clientY, 1 !== K(e) || e.metaKey || e.ctrlKey || (n = S(t = e.target)) && (i = n, a(), "mousedown" === e.type && (W(t) ? t.focus() : e.preventDefault())) } function x(e) { if (i) if (0 !== K(e)) { if (!(void 0 !== e.clientX && Math.abs(e.clientX - o) <= (y.slideFactorX || 0) && void 0 !== e.clientY && Math.abs(e.clientY - r) <= (y.slideFactorY || 0))) { if (y.ignoreInputTextSelection) { var t = ee("clientX", e) || 0, n = ee("clientY", e) || 0; if (W(R.elementFromPoint(t, n))) return } n = i; a(!0), b(), O(), C(n); n = function (e) { e = e.getBoundingClientRect(); return { left: e.left + z("scrollLeft", "pageXOffset"), top: e.top + z("scrollTop", "pageYOffset") } }(s); d = ee("pageX", e) - n.left, m = ee("pageY", e) - n.top, j.add(h || s, "gu-transit"), function () { if (l) return; var e = s.getBoundingClientRect(); (l = s.cloneNode(!0)).style.width = G(e) + "px", l.style.height = J(e) + "px", j.rm(l, "gu-transit"), j.add(l, "gu-mirror"), y.mirrorContainer.appendChild(l), U(q, "add", "mousemove", P), j.add(y.mirrorContainer, "gu-unselectable"), w.emit("cloned", l, s, "mirror") }(), P(e) } } else N({}) } function S(e) { if (!(w.dragging && l || u(e))) { for (var t = e; Q(e) && !1 === u(Q(e));) { if (y.invalid(e, t)) return; if (!(e = Q(e))) return } var n = Q(e); if (n) if (!y.invalid(e, t)) if (y.moves(e, n, t, Z(e))) return { item: e, source: n } } } function C(e) { var t, n; t = e.item, n = e.source, ("boolean" == typeof y.copy ? y.copy : y.copy(t, n)) && (h = e.item.cloneNode(!0), w.emit("cloned", h, e.item, "copy")), f = e.source, s = e.item, v = p = Z(e.item), w.dragging = !0, w.emit("drag", s, f) } function O() { var e; w.dragging && _(e = h || s, Q(e)) } function I() { a(!(i = !1)), b(!0) } function N(e) { var t, n; I(), w.dragging && (t = h || s, n = ee("clientX", e) || 0, e = ee("clientY", e) || 0, (e = B(H(l, n, e), n, e)) && (h && y.copySortSource || !h || e !== f) ? _(t, e) : (y.removeOnSpill ? X : L)()) } function _(e, t) { var n = Q(e); h && y.copySortSource && t === f && n.removeChild(s), A(t) ? w.emit("cancel", e, f, f) : w.emit("drop", e, t, f, p), Y() } function X() { var e, t; w.dragging && ((t = Q(e = h || s)) && t.removeChild(e), w.emit(h ? "cancel" : "remove", e, t, f), Y()) } function L(e) { var t, n, o; w.dragging && (t = 0 < arguments.length ? e : y.revertOnSpill, !1 === (e = A(o = Q(n = h || s))) && t && (h ? o && o.removeChild(h) : f.insertBefore(n, v)), e || t ? w.emit("cancel", n, f, f) : w.emit("drop", n, o, f, p), Y()) } function Y() { var e = h || s; I(), l && (j.rm(y.mirrorContainer, "gu-unselectable"), U(q, "remove", "mousemove", P), Q(l).removeChild(l), l = null), e && j.rm(e, "gu-transit"), n && clearTimeout(n), w.dragging = !1, g && w.emit("out", e, g, f), w.emit("dragend", e), f = s = h = v = p = n = g = null } function A(e, t) { t = void 0 !== t ? t : l ? p : Z(h || s); return e === f && t === v } function B(t, n, o) { for (var r = t; r && !function () { if (!1 === u(r)) return !1; var e = D(r, t), e = F(r, e, n, o); if (A(r, e)) return !0; return y.accepts(s, r, f, e) }();)r = Q(r); return r } function P(e) { if (l) { e.preventDefault(); var t = ee("clientX", e) || 0, n = ee("clientY", e) || 0, o = t - d, r = n - m; l.style.left = o + "px", l.style.top = r + "px"; var i = h || s, e = H(l, t, n), o = B(e, t, n), u = null !== o && o !== g; !u && null !== o || (g && a("out"), g = o, u && a("over")); r = Q(i); if (o !== f || !h || y.copySortSource) { var c, e = D(o, e); if (null !== e) c = F(o, e, t, n); else { if (!0 !== y.revertOnSpill || h) return void (h && r && r.removeChild(i)); c = v, o = f } (null === c && u || c !== i && c !== Z(i)) && (p = c, o.insertBefore(i, c), w.emit("shadow", i, o, f)) } else r && r.removeChild(i) } function a(e) { w.emit(e, i, g, f) } } function D(e, t) { for (var n = t; n !== e && Q(n) !== e;)n = Q(n); return n === q ? null : n } function F(r, t, i, u) { var c = "horizontal" === y.direction; return (t !== r ? function () { var e = t.getBoundingClientRect(); if (c) return n(i > e.left + G(e) / 2); return n(u > e.top + J(e) / 2) } : function () { var e, t, n, o = r.children.length; for (e = 0; e < o; e++) { if (t = r.children[e], n = t.getBoundingClientRect(), c && n.left + n.width / 2 > i) return t; if (!c && n.top + n.height / 2 > u) return t } return null })(); function n(e) { return e ? Z(t) : t } } } }).call(this, "undefined" != typeof global ? global : "undefined" != typeof self ? self : "undefined" != typeof window ? window : {}) }, { "./classes": 1, "contra/emitter": 5, crossvent: 6 }], 3: [function (e, t, n) { t.exports = function (e, t) { return Array.prototype.slice.call(e, t) } }, {}], 4: [function (e, t, n) { "use strict"; var o = e("ticky"); t.exports = function (e, t, n) { e && o(function () { e.apply(n || null, t || []) }) } }, { ticky: 10 }], 5: [function (e, t, n) { "use strict"; var c = e("atoa"), a = e("./debounce"); t.exports = function (r, e) { var i = e || {}, u = {}; return void 0 === r && (r = {}), r.on = function (e, t) { return u[e] ? u[e].push(t) : u[e] = [t], r }, r.once = function (e, t) { return t._once = !0, r.on(e, t), r }, r.off = function (e, t) { var n = arguments.length; if (1 === n) delete u[e]; else if (0 === n) u = {}; else { e = u[e]; if (!e) return r; e.splice(e.indexOf(t), 1) } return r }, r.emit = function () { var e = c(arguments); return r.emitterSnapshot(e.shift()).apply(this, e) }, r.emitterSnapshot = function (o) { var e = (u[o] || []).slice(0); return function () { var t = c(arguments), n = this || r; if ("error" === o && !1 !== i.throws && !e.length) throw 1 === t.length ? t[0] : t; return e.forEach(function (e) { i.async ? a(e, t, n) : e.apply(n, t), e._once && r.off(o, e) }), r } }, r } }, { "./debounce": 4, atoa: 3 }], 6: [function (n, o, e) { (function (r) { "use strict"; var i = n("custom-event"), u = n("./eventmap"), c = r.document, e = function (e, t, n, o) { return e.addEventListener(t, n, o) }, t = function (e, t, n, o) { return e.removeEventListener(t, n, o) }, a = []; function l(e, t, n) { t = function (e, t, n) { var o, r; for (o = 0; o < a.length; o++)if ((r = a[o]).element === e && r.type === t && r.fn === n) return o }(e, t, n); if (t) { n = a[t].wrapper; return a.splice(t, 1), n } } r.addEventListener || (e = function (e, t, n) { return e.attachEvent("on" + t, function (e, t, n) { var o = l(e, t, n) || function (n, o) { return function (e) { var t = e || r.event; t.target = t.target || t.srcElement, t.preventDefault = t.preventDefault || function () { t.returnValue = !1 }, t.stopPropagation = t.stopPropagation || function () { t.cancelBubble = !0 }, t.which = t.which || t.keyCode, o.call(n, t) } }(e, n); return a.push({ wrapper: o, element: e, type: t, fn: n }), o }(e, t, n)) }, t = function (e, t, n) { n = l(e, t, n); if (n) return e.detachEvent("on" + t, n) }), o.exports = { add: e, remove: t, fabricate: function (e, t, n) { var o = -1 === u.indexOf(t) ? new i(t, { detail: n }) : function () { var e; c.createEvent ? (e = c.createEvent("Event")).initEvent(t, !0, !0) : c.createEventObject && (e = c.createEventObject()); return e }(); e.dispatchEvent ? e.dispatchEvent(o) : e.fireEvent("on" + t, o) } } }).call(this, "undefined" != typeof global ? global : "undefined" != typeof self ? self : "undefined" != typeof window ? window : {}) }, { "./eventmap": 7, "custom-event": 8 }], 7: [function (e, r, t) { (function (e) { "use strict"; var t = [], n = "", o = /^on/; for (n in e) o.test(n) && t.push(n.slice(2)); r.exports = t }).call(this, "undefined" != typeof global ? global : "undefined" != typeof self ? self : "undefined" != typeof window ? window : {}) }, {}], 8: [function (e, n, t) { (function (e) { var t = e.CustomEvent; n.exports = function () { try { var e = new t("cat", { detail: { foo: "bar" } }); return "cat" === e.type && "bar" === e.detail.foo } catch (e) { } }() ? t : "undefined" != typeof document && "function" == typeof document.createEvent ? function (e, t) { var n = document.createEvent("CustomEvent"); return t ? n.initCustomEvent(e, t.bubbles, t.cancelable, t.detail) : n.initCustomEvent(e, !1, !1, void 0), n } : function (e, t) { var n = document.createEventObject(); return n.type = e, t ? (n.bubbles = Boolean(t.bubbles), n.cancelable = Boolean(t.cancelable), n.detail = t.detail) : (n.bubbles = !1, n.cancelable = !1, n.detail = void 0), n } }).call(this, "undefined" != typeof global ? global : "undefined" != typeof self ? self : "undefined" != typeof window ? window : {}) }, {}], 9: [function (e, t, n) { var o, r, t = t.exports = {}; function i() { throw new Error("setTimeout has not been defined") } function u() { throw new Error("clearTimeout has not been defined") } function c(t) { if (o === setTimeout) return setTimeout(t, 0); if ((o === i || !o) && setTimeout) return o = setTimeout, setTimeout(t, 0); try { return o(t, 0) } catch (e) { try { return o.call(null, t, 0) } catch (e) { return o.call(this, t, 0) } } } !function () { try { o = "function" == typeof setTimeout ? setTimeout : i } catch (e) { o = i } try { r = "function" == typeof clearTimeout ? clearTimeout : u } catch (e) { r = u } }(); var a, l = [], f = !1, s = -1; function d() { f && a && (f = !1, a.length ? l = a.concat(l) : s = -1, l.length && m()) } function m() { if (!f) { var e = c(d); f = !0; for (var t = l.length; t;) { for (a = l, l = []; ++s < t;)a && a[s].run(); s = -1, t = l.length } a = null, f = !1, function (t) { if (r === clearTimeout) return clearTimeout(t); if ((r === u || !r) && clearTimeout) return r = clearTimeout, clearTimeout(t); try { r(t) } catch (e) { try { return r.call(null, t) } catch (e) { return r.call(this, t) } } }(e) } } function v(e, t) { this.fun = e, this.array = t } function p() { } t.nextTick = function (e) { var t = new Array(arguments.length - 1); if (1 < arguments.length) for (var n = 1; n < arguments.length; n++)t[n - 1] = arguments[n]; l.push(new v(e, t)), 1 !== l.length || f || c(m) }, v.prototype.run = function () { this.fun.apply(null, this.array) }, t.title = "browser", t.browser = !0, t.env = {}, t.argv = [], t.version = "", t.versions = {}, t.on = p, t.addListener = p, t.once = p, t.off = p, t.removeListener = p, t.removeAllListeners = p, t.emit = p, t.prependListener = p, t.prependOnceListener = p, t.listeners = function (e) { return [] }, t.binding = function (e) { throw new Error("process.binding is not supported") }, t.cwd = function () { return "/" }, t.chdir = function (e) { throw new Error("process.chdir is not supported") }, t.umask = function () { return 0 } }, {}], 10: [function (e, n, t) { (function (t) { var e = "function" == typeof t ? function (e) { t(e) } : function (e) { setTimeout(e, 0) }; n.exports = e }).call(this, e("timers").setImmediate) }, { timers: 11 }], 11: [function (a, e, l) { (function (e, t) { var o = a("process/browser.js").nextTick, n = Function.prototype.apply, r = Array.prototype.slice, i = {}, u = 0; function c(e, t) { this._id = e, this._clearFn = t } l.setTimeout = function () { return new c(n.call(setTimeout, window, arguments), clearTimeout) }, l.setInterval = function () { return new c(n.call(setInterval, window, arguments), clearInterval) }, l.clearTimeout = l.clearInterval = function (e) { e.close() }, c.prototype.unref = c.prototype.ref = function () { }, c.prototype.close = function () { this._clearFn.call(window, this._id) }, l.enroll = function (e, t) { clearTimeout(e._idleTimeoutId), e._idleTimeout = t }, l.unenroll = function (e) { clearTimeout(e._idleTimeoutId), e._idleTimeout = -1 }, l._unrefActive = l.active = function (e) { clearTimeout(e._idleTimeoutId); var t = e._idleTimeout; 0 <= t && (e._idleTimeoutId = setTimeout(function () { e._onTimeout && e._onTimeout() }, t)) }, l.setImmediate = "function" == typeof e ? e : function (e) { var t = u++, n = !(arguments.length < 2) && r.call(arguments, 1); return i[t] = !0, o(function () { i[t] && (n ? e.apply(null, n) : e.call(null), l.clearImmediate(t)) }), t }, l.clearImmediate = "function" == typeof t ? t : function (e) { delete i[e] } }).call(this, a("timers").setImmediate, a("timers").clearImmediate) }, { "process/browser.js": 9, timers: 11 }] }, {}, [2])(2) });
</script>
<script>
/*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' });
</script>
<script>
// preact signals
!function (i, t) { "object" == typeof exports && "undefined" != typeof module ? t(exports) : "function" == typeof define && define.amd ? define(["exports"], t) : t((i || self).preactSignalsCore = {}) }(this, function (i) { function t() { throw new Error("Cycle detected") } var n = Symbol.for("preact-signals"); function o() { if (!(s > 1)) { var i, t = !1; while (void 0 !== h) { var n = h; h = void 0; e++; while (void 0 !== n) { var o = n.o; n.o = void 0; n.f &= -3; if (!(8 & n.f) && c(n)) try { n.c() } catch (n) { if (!t) { i = n; t = !0 } } n = o } } e = 0; s--; if (t) throw i } else s-- } var r = void 0, f = 0, h = void 0, s = 0, e = 0, v = 0; function u(i) { if (void 0 !== r) { var t = i.n; if (void 0 === t || t.t !== r) { t = { i: 0, S: i, p: r.s, n: void 0, t: r, e: void 0, x: void 0, r: t }; if (void 0 !== r.s) r.s.n = t; r.s = t; i.n = t; if (32 & r.f) i.S(t); return t } else if (-1 === t.i) { t.i = 0; if (void 0 !== t.n) { t.n.p = t.p; if (void 0 !== t.p) t.p.n = t.n; t.p = r.s; t.n = void 0; r.s.n = t; r.s = t } return t } } } function d(i) { this.v = i; this.i = 0; this.n = void 0; this.t = void 0 } d.prototype.brand = n; d.prototype.h = function () { return !0 }; d.prototype.S = function (i) { if (this.t !== i && void 0 === i.e) { i.x = this.t; if (void 0 !== this.t) this.t.e = i; this.t = i } }; d.prototype.U = function (i) { if (void 0 !== this.t) { var t = i.e, n = i.x; if (void 0 !== t) { t.x = n; i.e = void 0 } if (void 0 !== n) { n.e = t; i.x = void 0 } if (i === this.t) this.t = n } }; d.prototype.subscribe = function (i) { var t = this; return _(function () { var n = t.value, o = 32 & this.f; this.f &= -33; try { i(n) } finally { this.f |= o } }) }; d.prototype.valueOf = function () { return this.value }; d.prototype.toString = function () { return this.value + "" }; d.prototype.toJSON = function () { return this.value }; d.prototype.peek = function () { return this.v }; Object.defineProperty(d.prototype, "value", { get: function () { var i = u(this); if (void 0 !== i) i.i = this.i; return this.v }, set: function (i) { if (r instanceof y) !function () { throw new Error("Computed cannot have side-effects") }(); if (i !== this.v) { if (e > 100) t(); this.v = i; this.i++; v++; s++; try { for (var n = this.t; void 0 !== n; n = n.x)n.t.N() } finally { o() } } } }); function c(i) { for (var t = i.s; void 0 !== t; t = t.n)if (t.S.i !== t.i || !t.S.h() || t.S.i !== t.i) return !0; return !1 } function a(i) { for (var t = i.s; void 0 !== t; t = t.n) { var n = t.S.n; if (void 0 !== n) t.r = n; t.S.n = t; t.i = -1; if (void 0 === t.n) { i.s = t; break } } } function l(i) { var t = i.s, n = void 0; while (void 0 !== t) { var o = t.p; if (-1 === t.i) { t.S.U(t); if (void 0 !== o) o.n = t.n; if (void 0 !== t.n) t.n.p = o } else n = t; t.S.n = t.r; if (void 0 !== t.r) t.r = void 0; t = o } i.s = n } function y(i) { d.call(this, void 0); this.x = i; this.s = void 0; this.g = v - 1; this.f = 4 } (y.prototype = new d).h = function () { this.f &= -3; if (1 & this.f) return !1; if (32 == (36 & this.f)) return !0; this.f &= -5; if (this.g === v) return !0; this.g = v; this.f |= 1; if (this.i > 0 && !c(this)) { this.f &= -2; return !0 } var i = r; try { a(this); r = this; var t = this.x(); if (16 & this.f || this.v !== t || 0 === this.i) { this.v = t; this.f &= -17; this.i++ } } catch (i) { this.v = i; this.f |= 16; this.i++ } r = i; l(this); this.f &= -2; return !0 }; y.prototype.S = function (i) { if (void 0 === this.t) { this.f |= 36; for (var t = this.s; void 0 !== t; t = t.n)t.S.S(t) } d.prototype.S.call(this, i) }; y.prototype.U = function (i) { if (void 0 !== this.t) { d.prototype.U.call(this, i); if (void 0 === this.t) { this.f &= -33; for (var t = this.s; void 0 !== t; t = t.n)t.S.U(t) } } }; y.prototype.N = function () { if (!(2 & this.f)) { this.f |= 6; for (var i = this.t; void 0 !== i; i = i.x)i.t.N() } }; y.prototype.peek = function () { if (!this.h()) t(); if (16 & this.f) throw this.v; return this.v }; Object.defineProperty(y.prototype, "value", { get: function () { if (1 & this.f) t(); var i = u(this); this.h(); if (void 0 !== i) i.i = this.i; if (16 & this.f) throw this.v; return this.v } }); function w(i) { var t = i.u; i.u = void 0; if ("function" == typeof t) { s++; var n = r; r = void 0; try { t() } catch (t) { i.f &= -2; i.f |= 8; p(i); throw t } finally { r = n; o() } } } function p(i) { for (var t = i.s; void 0 !== t; t = t.n)t.S.U(t); i.x = void 0; i.s = void 0; w(i) } function b(i) { if (r !== this) throw new Error("Out-of-order effect"); l(this); r = i; this.f &= -2; if (8 & this.f) p(this); o() } function g(i) { this.x = i; this.u = void 0; this.s = void 0; this.o = void 0; this.f = 32 } g.prototype.c = function () { var i = this.S(); try { if (8 & this.f) return; if (void 0 === this.x) return; var t = this.x(); if ("function" == typeof t) this.u = t } finally { i() } }; g.prototype.S = function () { if (1 & this.f) t(); this.f |= 1; this.f &= -9; w(this); a(this); s++; var i = r; r = this; return b.bind(this, i) }; g.prototype.N = function () { if (!(2 & this.f)) { this.f |= 2; this.o = h; h = this } }; g.prototype.d = function () { this.f |= 8; if (!(1 & this.f)) p(this) }; function _(i) { var t = new g(i); try { t.c() } catch (i) { t.d(); throw i } return t.d.bind(t) } i.Signal = d; i.batch = function (i) { if (s > 0) return i(); s++; try { return i() } finally { o() } }; i.computed = function (i) { return new y(i) }; i.effect = _; i.signal = function (i) { return new d(i) }; i.untracked = function (i) { if (f > 0) return i(); var t = r; r = void 0; f++; try { return i() } finally { f--; r = t } } });//# sourceMappingURL=signals-core.min.js.map
</script>
<script id="default_ui_library">
"use strict";
// Global variables
const { html, render: renderElem } = uhtml;
const { signal, computed, effect } = preactSignalsCore;
//Checks for internet connection status
if (!navigator.onLine)
floGlobals.connectionErrorNotification = notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error')
window.addEventListener('offline', () => {
floGlobals.connectionErrorNotification = notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error')
})
window.addEventListener('online', () => {
getRef('notification_drawer').remove(floGlobals.connectionErrorNotification)
notify('We are back online.', 'success')
})
function getRef(elementId) {
return document.getElementById(elementId);
}
// 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}`)
return getRef(popupId).show({ pinned })
}
// hides the popup or modal
function closePopup(options = {}) {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide(options)
}
// 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', danger = false } = options
getRef('confirm_title').innerText = title;
getRef('confirm_message').innerText = message;
const cancelButton = getRef('confirmation_popup').querySelector('.cancel-button');
const confirmButton = getRef('confirmation_popup').querySelector('.confirm-button')
confirmButton.textContent = confirmText
cancelButton.textContent = cancelText
if (danger)
confirmButton.classList.add('button--danger')
else
confirmButton.classList.remove('button--danger')
const { opened, closed } = openPopup('confirmation_popup')
confirmButton.onclick = () => {
closePopup({ payload: true })
}
cancelButton.onclick = () => {
closePopup()
}
closed.then((payload) => {
confirmButton.onclick = null
cancelButton.onclick = null
if (payload)
resolve(true)
else
resolve(false)
})
})
}
//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 = `<svg class="icon icon--success" 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>`
break;
case 'error':
icon = `<svg class="icon icon--error" 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 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>`
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]), .interactive")) {
createRipple(e, e.target.closest("button, .interactive"));
}
});
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;
}
}
function getObjLength(obj) {
let count = 0
for (var i in obj) count++
return count
}
// obj forEach
function objForEach(obj, callback) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
callback(key, obj[key])
}
}
}
// obj map
function objMap(obj, callback) {
const newObj = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
const value = callback(obj[key], key)
if (value) {
newObj.push(value)
}
}
}
return newObj
}
const appState = {
params: {},
}
let dragger
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('/')) {
let path;
[path, searchParams] = targetPage.split('?');
[, pageId, subPageId1] = path.split('/')
} else {
pageId = targetPage
}
}
if (!document.querySelector(`#${pageId}`)?.classList.contains('inner-page')) return
try {
if (floDapps.user.id && (!location.hash || generalPages.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 (!getRef('landing_tasks_wrapper')) return
renderElem(getRef('landing_tasks_wrapper'), render.displayTasks(params?.category, params?.search))
if (params?.taskId) {
showTaskDetails(params.taskId)
} else {
hideTaskDetails()
}
break;
case 'sign_up':
getRef('keys_generator').generateKeys()
break;
case 'dashboard_page': {
let renderedAssignedTasks = []
if (userType === 'intern') {
// Render assigned task cards
const { assignedTasks } = RIBC.getInternRecord(floGlobals.myFloID)
for (const taskId in assignedTasks)
renderedAssignedTasks.push(render.internTaskCard(taskId))
if (renderedAssignedTasks.length === 0) {
renderedAssignedTasks = html`No task assigned yet.`;
}
} else {
if (subPageId1 === 'my_certificates') {
subPageId1 = undefined
history.replaceState(null, null, '#/dashboard_page')
}
}
if (!subPageId1) {
if (userType === 'intern')
subPageId1 = 'my_tasks'
else if (userType === 'admin')
subPageId1 = 'active_tasks'
else
subPageId1 = 'all_tasks'
}
//creates cards for highest performing interns
renderElem(getRef('dashboard_page'), html`
<sm-chips id="dashboard_view_selector" class="margin-right-auto" onchange=${handleDashboardViewChange}>
${userType === 'intern' ? html`<sm-chip value="intern_view" ?selected=${subPageId1 === 'my_tasks'}>My tasks</sm-chip>` : ''}
${userType !== 'admin' ? html`<sm-chip value="dashboard_tasks_wrapper" ?selected=${subPageId1 === 'all_tasks'}>All tasks</sm-chip>` : ''}
${userType === 'admin' ? html`<sm-chip value="active_tasks_wrapper" ?selected=${subPageId1 === 'active_tasks'}>Active tasks</sm-chip>` : ''}
<sm-chip value="projects_wrapper" ?selected=${subPageId1 === 'projects'}>Projects</sm-chip>
${userType === 'intern' ? html`<sm-chip value="certificates_view" ?selected=${subPageId1 === 'my_certificates'}>My certificates</sm-chip>` : ''}
${floGlobals.isMobileView ? html`<sm-chip value="intern_leaderboard_container">Leaderboard</sm-chip>` : ''}
</sm-chips>
${userType === 'intern' ? html`
<section id="intern_view" class="intern-option dashboard-view__item">
<ul id="assigned_task_list">${renderedAssignedTasks}</ul>
</section>
` : ''}
${userType !== 'admin' ? html`<div id="dashboard_tasks_wrapper" class=${`flex flex-direction-column justify-content-center dashboard-view__item ${userType === 'intern' ? 'hidden' : ''}`}>${render.displayTasks(params?.category, params?.search)}</div>` : ''}
${userType === 'admin' ? html`<div id="active_tasks_wrapper" class=${`flex flex-direction-column gap-0-5 justify-content-center dashboard-view__item`}>${render.activeTasks()}</div>` : ''}
<div id="projects_wrapper" class="grid gap-2 align-items-start align-content-start dashboard-view__item hidden">${render.dashProjects()}</div>
<div id="intern_leaderboard_container" class="dashboard-view__item hide-on-mobile">
<div class="container-header">
<svg class="icon margin-right-0-5" 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"><rect fill="none" height="24" width="24"/><g><path d="M7.5,21H2V9h5.5V21z M14.75,3h-5.5v18h5.5V3z M22,11h-5.5v10H22V11z"/></g></svg>
<h4>Leaderboard</h4>
<a id="all_interns_btn" href="#/all_interns_page" class="button button--small">All</a>
</div>
<div id="top_interns" class="observe-empty-state">${filterInterns('', { sortByRating: true, activeOnly: true, limit: 8 })}</div>
<div class="empty-state">
<h4>There are no interns</h4>
</div>
</div>
<div id="certificates_view" class="dashboard-view__item hidden">
<ul id="certificates_container" class="grid gap-1"></ul>
</div>
`)
changeDashboardView(getKeyByValue(floGlobals.dashboardPages, subPageId1))
if (params?.taskId) {
showTaskDetails(params.taskId)
} else
hideTaskDetails()
if (subPageId1 === 'my_certificates' && userType === 'intern') {
effect(() => {
if (fetchCertificateIssueTxsState.value === 'done') {
const issuedCertificates = []
floGlobals.validCerts.forEach((cert, id) => {
if (cert.floId === floGlobals.myFloID) {
issuedCertificates.push(html`
<li class="certificate-card flex align-center gap-0-5">
<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 4H4c-1.1 0-2 .9-2 2v12.01c0 1.1.9 1.99 2 1.99h16c1.1 0 2-.9 2-2v-8l-6-6zM4 18.01V6h11v5h5v7.01H4z"/></svg>
<h4 class="capitalize">${cert.certType.toLowerCase()}</h4>
<a class="button button--small margin-left-auto gap-0-3" href=${`https://ranchimall.github.io/certify/#/home/donwload?id=${cert.txid}`} target="_blank">
<svg 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><path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/></g></svg>
Download
</a>
</li>
`)
}
})
if (issuedCertificates.length === 0) {
renderElem(getRef('certificates_container'), html`
<li class="flex align-center gap-0-5" style="padding: 1rem 0">
<h4>You have not received any certificates yet.</h4>
</li>
`)
} else {
renderElem(getRef('certificates_container'), html`${issuedCertificates}`)
}
} else if (fetchCertificateIssueTxsState.value === 'fetching') {
renderElem(getRef('certificates_container'), html`
<li class="flex align-center gap-0-5" style="padding: 1rem 0">
<sm-spinner></sm-spinner>
<h4>Fetching certificates...</h4>
</li>
`)
}
})
}
}
break;
case 'updates_page': {
closePopup()
if (!getRef('updates_page__project_selector').children.length) {
renderProjectSelectorOptions()
renderInternSelectorOptions()
}
const { projectCode = 'all', internId = 'all', date } = params || {}
setUpdateFilters({ projectCode, internId, date })
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 (params?.taskId) {
showTaskDetails(params.taskId)
} else {
hideTaskDetails()
}
break;
case 'all_interns_page':
renderAllInterns()
break;
case 'intern_profile':
render.internProfile(params?.id)
break;
case 'project_explorer':
let breadcrumbs = []
if (subPageId1) {
if (params) {
const { id: projectCode, branch } = params
const { projectName, projectDescription } = RIBC.getProjectDetails(projectCode);
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('hidden')
breadcrumbs = [
['Dashboard', '#/dashboard_page/projects'],
['All projects', '#/project_explorer/projects'],
[projectName, `#/project_explorer/project?id=${projectCode}&branch=mainLine`],
]
} else {
getRef('project_explorer__left').querySelectorAll('.project-card').forEach(project => project.classList.remove('project-card--active'))
getRef('project_explorer__left').classList.remove('hide-on-mobile')
getRef('project_explorer__right').classList.add('hidden')
breadcrumbs = [
['Dashboard', '#/dashboard_page/projects'],
['All projects', '#/project_explorer/projects']
]
}
} else {
getRef('project_explorer__left').classList.remove('hide-on-mobile')
getRef('project_explorer__right').classList.add('hidden')
history.replaceState(null, '', '#/project_explorer/projects')
breadcrumbs = [
['Dashboard', '#/dashboard_page/projects'],
]
}
renderElem(getRef('project_explorer__breadcrumbs'), html`${createBreadcrumbs(breadcrumbs)}`)
break;
case 'admin_page':
if (userType !== 'admin') return;
//show projects
if (subPageId1) {
getRef('admin_view_selector').value = subPageId1;
const viewIndex = [...getRef('admin_view_selector').children].findIndex(elem => elem.hasAttribute('selected'))
showChildElement(getRef('admin_views'), viewIndex, { entry: viewIndex > currentViewIndex ? slideInLeft : slideInRight, exit: viewIndex > currentViewIndex ? slideOutLeft : slideOutRight });
currentViewIndex = viewIndex;
switch (subPageId1) {
case 'projects':
render.projectList(getRef('admin_page__project_list'), getSortedProjectList(), true)
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/projects')
getRef('admin_page__project_list').querySelectorAll('.project-card').forEach(project => project.classList.remove('project-card--active'))
}
break;
case 'interns':
//show interns
render.adminInterns()
break;
case 'requests':
function removeRequest(requestCard) {
requestCard.animate([
{
transform: 'translateX(0)',
opacity: 1
},
{
transform: 'translateX(-100%)',
opacity: 0
},
], {
duration: floGlobals.prefersReducedMotion ? 0 : 150,
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?', { confirmText: 'Accept' }).then(result => {
if (result) {
const vectorClock = e.delegateTarget.closest('.request-card').dataset.vectorClock
if (RIBC.getInternList())
RIBC.admin.processTaskRequest(vectorClock, true).then(() => {
notify('Intern assigned, commit changes to make it permanent.', 'success')
removeRequest(e.delegateTarget.closest('.request-card'))
adminDataChanged();
}).catch(err => {
notify(err, 'error')
})
}
})
})
// reject task request
delegate(getRef('requests_list'), 'click', '.reject-request', (e) => {
getConfirmation('Are you sure you want to reject this request?', { confirmText: 'Reject' }).then((result) => {
if (result) {
const vectorClock = e.delegateTarget.closest('.request-card').dataset.vectorClock
const type = e.delegateTarget.closest('.request-card').dataset.type
if (type === 'task') {
RIBC.admin.processTaskRequest(vectorClock, false).then(() => {
notify('Request rejected', 'success')
removeRequest(e.delegateTarget.closest('.request-card'))
adminDataChanged();
}).catch(err => {
notify(err, 'error')
})
} else if (type === 'internship') {
const result = RIBC.admin.processInternRequest(vectorClock, false)
if (result === 'Rejected') {
notify('Request rejected', 'success')
removeRequest(e.delegateTarget.closest('.request-card'))
adminDataChanged();
}
}
}
})
})
break;
case 'task_display':
//show display tasks map
render.taskDisplayMap()
if (dragger)
dragger.destroy()
dragger = dragula([getRef('display_task_map'), getRef('all_tasks')])
dragger.on('dragend', function (el, source) {
const newOrder = Array.from(getRef('display_task_map').children).map(el => el.dataset.taskId)
RIBC.admin.setDisplayedTasks(newOrder)
adminDataChanged();
});
break;
case 'certificates':
const issuedCertificates = [...floGlobals.validCerts.values()]
.sort((a, b) => a.name.localeCompare(b.name))
.map((cert) => {
const { name, certType, floId } = cert;
let initials = name.split(' ').map(word => word[0]).join('')
initials = initials.length === 1 ? initials : initials[0] + initials[initials.length - 1]
return html`
<li class="intern-card align-center flex space-between gap-1">
<div class="intern-card__initials" style=${`--color: var(${getInternColor(floId)})`}>${initials}</div>
<span>${name}</span>
<span>${certType.toLowerCase()}</span>
</li>`
})
renderElem(getRef('certificates_list'), html`${issuedCertificates}`)
break;
}
}
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')
getRef('main_nav').classList.remove('hide-on-mobile')
} else {
getRef('main_nav').classList.add('hide-on-mobile')
}
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 : 150, 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 : 150, fill: 'forwards', easing: 'ease' }).onfinish = (e) => {
getRef(pageId).parentNode.style.overflow = ogOverflow;
switch (pageId) {
case 'sign_in':
getRef('private_key_field').focusIn()
break;
}
if (appState.lastPage === 'dashboard_page') {
renderElem(getRef('dashboard_page'), html``)
}
if (appState.lastPage === 'intern_profile') {
renderElem(getRef('intern_profile'), html``)
}
if (appState.lastPage === 'settings_page') {
renderElem(getRef('settings_page'), html``)
}
appState.lastPage = pageId
}
}
}
function createBreadcrumbs(links) {
const crumbs = []
links.forEach(([name, link], index) => {
crumbs.push(html`<a href="${link}" class=${`breadcrumb ${(index === links.length - 1) ? 'breadcrumb--active' : ''}`}>${name}</a>`)
if (index !== links.length - 1) {
crumbs.push(html`
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
`)
}
})
return crumbs
}
// 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' ? getRef(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
const generateKeys = document.createElement('template')
generateKeys.innerHTML = `
<style>
:host{
display: flex;
justify-content: center;
}
.generated-keys-wrapper {
padding: 1rem;
background-color: rgba(var(--foreground-color), 1);
border-radius: 0.5rem;
}
#flo_id_warning{
padding-bottom: 1.5rem;
}
#flo_id_warning .icon {
height: 3rem;
width: 3rem;
padding: 0.8rem;
overflow: visible;
background-color: #ffc107;
border-radius: 3rem;
fill: rgba(0, 0, 0, 0.8);
}
</style>
<section class="grid gap-1-5">
<div id="flo_id_warning" class="flex gap-1">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0z" fill="none" /> <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" /> </svg>
<div class="grid gap-0-5">
<strong>
<h3> Keep your keys safe! </h3>
</strong>
<p>Don't share with anyone. Once lost private key can't be recovered.</p>
</div>
</div>
<div class="grid gap-1-5 generated-keys-wrapper">
<div class="grid gap-0-5">
<h5>FLO address</h5>
<sm-copy id="generated_flo_address"></sm-copy>
</div>
<div class="grid gap-0-5">
<h5>Private key</h5>
<sm-copy id="generated_private_key"></sm-copy>
</div>
</div>
<button id="sign_up_button" class="button button--primary w-100">Sign in with these credentials</button>
<p class="margin-top-1">You can use these FLO credentials with other RanchiMall apps too. </p>
</section>
`
window.customElements.define('keys-generator', class extends HTMLElement {
constructor() {
super();
this.appendChild(generateKeys.content.cloneNode(true));
}
get keys() {
return {
floID: this.querySelector('#generated_flo_address').value,
privKey: this.querySelector('#generated_private_key').value
}
}
generateKeys() {
const { floID, privKey } = floCrypto.generateNewID()
this.querySelector('#generated_flo_address').value = floID
this.querySelector('#generated_private_key').value = privKey
}
clearKeys() {
this.querySelector('#generated_flo_address').value = ''
this.querySelector('#generated_private_key').value = ''
}
});
</script>
<script id="app_ui">
"use strict";
floGlobals.taskCategories = {
c00: 'Creative Writing',
c01: 'Marketing',
c02: 'Design',
c03: 'Development',
c04: 'Social Media Management',
c05: 'Video Making',
c06: 'Project Scouting & Capital Raising',
c07: 'Investment & Finance',
}
const render = {
displayTaskCard(projectCode, branch, task) {
const taskId = `${projectCode}_${branch}_${task}`;
const { title, category, maxSlots, duration, durationType, reward } = RIBC.getTaskDetails(taskId)
const assignedInterns = RIBC.getAssignedInterns(taskId);
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=${`${location.hash.split('?')[0]}?taskId=${taskId}`} 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>
<a href=${`${location.hash.split('?')[0]}?taskId=${taskId}`} class="display-task__link flex flex-direction-column gap-1">
<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 - assignedInterns.length}</span>
</div>
`: ''}
${reward ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Reward: </span>
<span class="display-task__detail__value">₹${reward}</span>
</div>
`: ''}
</div>
</a>
</li>
`;
},
displayTasks(category = 'all', searchQuery) {
// render tasks
const allTasks = RIBC.getAllTasks();
if (!allTasks) return;
const displayedTasks = RIBC.getDisplayedTasks()
const filterCategory = category === 'all' ? false : category;
const filtered = []
const availableCategories = new Set();
for (const taskId of displayedTasks) {
const [projectCode, branch, task] = taskId.split('_')
const assignedInterns = RIBC.getAssignedInterns(taskId);
if (assignedInterns.length >= allTasks[taskId].maxSlots) continue;
availableCategories.add(allTasks[taskId].category)
if (userType && userType === 'intern' && floGlobals.myFloID && assignedInterns.includes(floGlobals.myFloID)) continue;
if (filterCategory && allTasks[taskId].category !== filterCategory) continue;
if (searchQuery && searchQuery !== '') {
if (!allTasks[taskId].title.toLowerCase().includes(searchQuery.toLowerCase()) && !floGlobals.taskCategories[allTasks[taskId].category].toLowerCase().includes(searchQuery.toLowerCase())) continue;
}
filtered.push(render.displayTaskCard(projectCode, branch, task))
}
let renderedTasks = filtered
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`<sm-chip value='all' ?selected=${category === 'all'}>All</sm-chip>`];
availableCategories.forEach(categoryID => {
renderedCategories.push(html`<sm-chip value=${categoryID} ?selected=${categoryID === category}>${floGlobals.taskCategories[categoryID]}</sm-chip>`)
})
}
setTimeout(() => {
if (getRef('task_search_input') && getRef('task_search_input').value.trim() !== searchQuery)
getRef('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">
<h2>
Apply below
</h2>
${(filtered.length > 0 || searchQuery) ? html`
<sm-input id="task_search_input" oninput="filterTasks(event)" placeholder="Find tasks" 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`<sm-chips id="task_category_selector" onchange='filterTasks()'>${renderedCategories}</sm-chips>` : ''}
</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'
const projectLink = isAdmin ? `#/${page}/projects?id=${projectCode}&branch=mainLine` : `#/${page}/project?id=${projectCode}&branch=mainLine`
return html.for(ref, projectCode)`<a class="project-card flex align-center interactive" title="Project information" href=${projectLink}>${projectName}</a>`
},
taskCard(task) {
const taskId = `${appState.params.id}_${appState.params.branch}_${task}`;
const { title, description, category, maxSlots, duration, durationType, reward } = RIBC.getTaskDetails(taskId)
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(taskId)
const assignedInternsCards = assignedInterns.map(internFloId => render.assignedInternCard(internFloId, taskId));
const status = RIBC.getTaskStatus(taskId)
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">
<h4 class="timeline-task__title">${title}</h4>
${assignedInternsCards.length ? html`<div class="flex align-center gap-0-3 flex-wrap">${assignedInternsCards}</div>` : ''}
<collapsed-text>
${linkifyDescription}
</collapsed-text>
<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 - assignedInterns.length}</span>
</div>
`: ''}
${reward ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Reward: </span>
<span class="display-task__detail__value">₹${reward}</span>
</div>
`: ''}
${branchesButtons.length ? html`<div class="task__branch_container">${branchesButtons}</div>` : ''}
</div>
</div>
`;
},
activeTasks() {
return Object.keys(RIBC.getAllTasks())
.filter(task => RIBC.getTaskStatus(task) === 'incomplete')
.map(task => render.activeTaskCard(task))
},
activeTaskCard(taskId) {
const { title } = RIBC.getTaskDetails(taskId)
const assignedInterns = (RIBC.getAssignedInterns(taskId) || []).map(internFloId => render.assignedInternCard(internFloId, taskId))
return html`
<div class="active-task grid align-center gap-0-5">
<h4 class="active-task-card__title">${title}</h4>
<div class="active-task-card__interns flex align-center gap-0-5">
${assignedInterns.length ? assignedInterns : html`<p class="active-task-card__no-interns">No Interns Assigned</p>`}
</div>
</div>
`
},
internCard(internFloId, options = {}) {
const { selectable } = options
const internName = RIBC.getInternList()[internFloId]
const internPoints = RIBC.getInternRating(internFloId)
const { active } = RIBC.getInternRecord(internFloId)
const splitName = internName.split(' ')
let initials = splitName[0][0]
if (splitName.length > 1) {
initials += splitName[splitName.length - 1][0]
}
if (selectable) {
return html`
<label class="intern-card align-center interactive" .dataset=${{ internFloId }} title="Intern Information">
<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 flex-1">${internName}</div>
<div class="intern-card__score-wrapper flex align-center">
<b class="intern-card__score">${internPoints}</b>
<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>`;
} else {
return html`
<a class="intern-card align-center interactive" href=${`#/intern_profile?id=${internFloId}`} title="Intern Information">
<div class="intern-card__initials" style=${`--color: var(${getInternColor(internFloId)})`}>${initials}</div>
<div class="grid gap-0-3 flex-1">
<div class="intern-card__name">${internName}</div>
${!active ? html`<div class="intern-card__status">Inactive</div>` : ''}
</div>
<div class="intern-card__score-wrapper flex align-center">
<b class="intern-card__score">${internPoints}</b>
<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>
</a>`;
}
},
adminInterns() {
const addInternButton = html`<button class="button button--colored" onclick="openPopup('add_intern_popup')">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" 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 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/></svg>
Add intern
</button>`
renderElem(getRef('admin_page__intern_list'), html`${[addInternButton, filterInterns('')]}`)
},
internUpdateCard(update) {
const { floID, time, note, update: { projectCode, branch, task, description, link }, tag } = update
let topic = `${RIBC.getProjectDetails(projectCode).projectName} / ${RIBC.getTaskDetails(`${projectCode}_${branch}_${task}`).title}`
const internName = RIBC.getInternList()[floID]
const updateMessage = createElement('p', {
className: 'update__message ws-pre-line wrap-around',
innerHTML: DOMPurify.sanitize(linkify(description))
})
let replyButton
let saveButton
if (userType === "admin") {
if (!note)
replyButton = html`<button class="button button--small button--colored init-update-replay">Reply</button>`
if (!tag)
saveButton = html`<button class="button button--small button--colored save-update margin-left-auto">Save</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>
${updateMessage}
${providedLink}
${adminReply}
${saveButton || replyButton ? html`<div class="flex align-center gap-0-3">${saveButton}${replyButton}</div>` : ''}
</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, taskId, options = {}) {
const { showOptions = false } = options
const { hasDeadlinePassed, elapsedPercentage, taskDeadline } = getTaskDeadline(taskId, internFloId)
const taskCompleted = RIBC.getInternRecord(internFloId).completedTasks.hasOwnProperty(taskId)
let taskStatus = 'In Progress';
let statusModifier = '--in-progress'
if (taskCompleted) {
taskStatus = 'Completed'
statusModifier = '--completed'
} else if (hasDeadlinePassed) {
taskStatus = 'Overdue'
statusModifier = '--overdue'
}
let isInProgress = !taskCompleted && !hasDeadlinePassed
let optionsButton
if (showOptions && taskId && !taskCompleted) {
optionsButton = html` <button class="unassign-intern-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> `;
}
const title = isInProgress ? `Due ${getFormattedTime(taskDeadline, 'date-only')}` : ''
return html`
<span class=${`assigned-intern assigned-intern${statusModifier}`} title=${title} data-flo-id="${internFloId}" style=${`--progress: ${elapsedPercentage}`}>
${RIBC.getInternList()[internFloId]}
<span class=${`task-status task-status${statusModifier}`}>${taskStatus}</span>
${optionsButton}
</span>
`
},
assignedInternTasks(internId) {
const { assignedTasks, completedTasks, failedTasks } = RIBC.getInternRecord(internId)
if (getObjLength(assignedTasks) === 0) return false
const assignedTasksList = [];
for (const task in assignedTasks) {
if (completedTasks.hasOwnProperty(task) || failedTasks.hasOwnProperty(task)) continue;
const { assignedOn } = assignedTasks[task];
const { title } = RIBC.getAllTasks()[task];
assignedTasksList.push(html`
<div class="intern_profile__task intern_profile__task--assigned">
<h4>${title}</h4>
<time>${getFormattedTime(assignedOn || assignedTasks[task], 'date-only')}</time>
</div>`)
}
return assignedTasksList.length > 0 ? assignedTasksList : false
},
completedInternTasks(internId, savedUpdates) {
const { completedTasks } = RIBC.getInternRecord(internId)
if (getObjLength(completedTasks) === 0) return false
return Object.keys(completedTasks).map(task => {
const { link, description } = savedUpdates.get(task) || {}
const { points, completionDate } = completedTasks[task];
const { title } = RIBC.getAllTasks()[task];
const div = document.createElement('div')
div.innerHTML = DOMPurify.sanitize(linkify(link || description))
const links = [...div.querySelectorAll('a')].map(link => {
link.textContent = 'See output'
link.className = 'button button--small button--colored margin-right-auto'
return link
})
return html`
<div class="intern_profile__task intern_profile__task--completed">
<h4>${title}</h4>
<time>${getFormattedTime(completionDate, 'date-only')}</time>
<p class="flex align-center gap-0-3">
${points}
<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> <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"></path> </svg>
</p>
${links}
</div>`})
},
adminTask(task) {
const taskId = `${appState.params.id}_${appState.params.branch}_${task}`
const assignedInterns = RIBC.getAssignedInterns(taskId)
const status = RIBC.getTaskStatus(taskId)
const { title, description, category, maxSlots, duration, durationType, reward } = RIBC.getTaskDetails(taskId)
let assignedInternsCards = assignedInterns.map(internFloId => render.assignedInternCard(internFloId, taskId, { showOptions: 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'
})
}
})
if (status === 'incomplete') {
const taskTitle = createElement('h4', {
className: 'task-title',
innerHTML: DOMPurify.sanitize(title)
})
const taskDescription = createElement('p', {
className: 'task-description ws-pre-line wrap-around',
innerHTML: DOMPurify.sanitize(description)
})
return html`
<li class=${`admin-task ${status}`} .dataset=${{ taskId: task }}>
<div class="flex align-center gap-0-5 flex-wrap">
<div class="admin-task__task-number margin-right-auto">ID: ${task}</div>
${assignedInternsCards.length ? html`<button class="button button--small button--colored" onclick=${markTaskAsCompleted}>
<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 0h24v24H0z" fill="none" /> <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" /> </svg>
Mark as done
</button>` : ''}
<button class="button icon-only button--colored" title="Edit task" onclick=${openTaskEditingPopup}>
<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> <path d="M15.728 9.686l-1.414-1.414L5 17.586V19h1.414l9.314-9.314zm1.414-1.414l1.414-1.414-1.414-1.414-1.414 1.414 1.414 1.414zM7.242 21H3v-4.243L16.435 3.322a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L7.243 21z"></path> </svg>
</button>
<button class="button button--danger icon-only" title="Delete this task" 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>
</div>
${taskTitle}
<div class="grid gap-1 intern-section">
<div class="flex align-center gap-0-5">
<button class="button button--small button--colored" onclick="currentTask=this.closest('.admin-task');openPopup('intern_list_popup')">
<svg class="icon margin-right-0-3" 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><path d="M20,9V6h-2v3h-3v2h3v3h2v-3h3V9H20z M9,12c2.21,0,4-1.79,4-4c0-2.21-1.79-4-4-4S5,5.79,5,8C5,10.21,6.79,12,9,12z M9,6 c1.1,0,2,0.9,2,2c0,1.1-0.9,2-2,2S7,9.1,7,8C7,6.9,7.9,6,9,6z M15.39,14.56C13.71,13.7,11.53,13,9,13c-2.53,0-4.71,0.7-6.39,1.56 C1.61,15.07,1,16.1,1,17.22V20h16v-2.78C17,16.1,16.39,15.07,15.39,14.56z M15,18H3v-0.78c0-0.38,0.2-0.72,0.52-0.88 C4.71,15.73,6.63,15,9,15c2.37,0,4.29,0.73,5.48,1.34C14.8,16.5,15,16.84,15,17.22V18z"/></g></svg>
Assign
</button>
${assignedInternsCards.length ? html`<button class="button button--small button--colored" onclick=${initTaskScoring}>
<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 0h24v24H0z" fill="none"/><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></svg>
Rate
</button>` : ''}
</div>
${assignedInternsCards.length ? html`<div class="flex align-center flex-wrap gap-0-3"> ${assignedInternsCards} </div>` : 'No interns assigned. Click on "Assign" to assign interns.'}
</div>
<collapsed-text>
${taskDescription}
${branchesButtons.length ? html`<div class="task__branch_container">${branchesButtons}</div>` : ''}
<div class="display-task__details flex flex-wrap gap-0-3 margin-top-1">
<div class="display-task__detail">
<span class="display-task__detail__title">Category: </span>
<span class="display-task__detail__value">${floGlobals.taskCategories[category]}</span>
</div>
<div class="display-task__detail">
<span class="display-task__detail__title">Duration: </span>
<span class="display-task__detail__value">${duration} ${durationType}</span>
</div>
<div class="display-task__detail">
<span class="display-task__detail__title">Max slots: </span>
<span class="display-task__detail__value">${maxSlots}</span>
</div>
<div class="display-task__detail">
<span class="display-task__detail__title">Reward: </span>
<span class="display-task__detail__value">${reward}</span>
</div>
</div>
</collapsed-text>
${branchesButtons.length ? html`<div class="task__branch_container">${branchesButtons}</div>` : ''}
<div class="flex align-center gap-0-5 flex-wrap margin-top-1">
<button class="button button--small button--colored" onclick=${openNewBranchPopup}>
<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="M7.105 15.21A3.001 3.001 0 1 1 5 15.17V8.83a3.001 3.001 0 1 1 2 0V12c.836-.628 1.874-1 3-1h4a3.001 3.001 0 0 0 2.895-2.21 3.001 3.001 0 1 1 2.032.064A5.001 5.001 0 0 1 14 13h-4a3.001 3.001 0 0 0-2.895 2.21zM6 17a1 1 0 1 0 0 2 1 1 0 0 0 0-2zM6 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm12 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2z" /> </svg>
Create Branch
</button>
</div>
</li>
`;
} else {
const taskDescription = createElement('p', {
className: 'task-description ws-pre-line wrap-around',
innerHTML: DOMPurify.sanitize(linkify(description))
})
return html`
<li class=${`admin-task ${status}`} .dataset=${{ taskId: task }}>
<div class="flex align-center gap-0-3 space-between">
<button class="button button--small button--danger" onclick=${markTaskAsIncomplete}>
<svg class="icon margin-right-0-3" 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"/><path d="M17,12c-2.76,0-5,2.24-5,5s2.24,5,5,5c2.76,0,5-2.24,5-5S19.76,12,17,12z M18.65,19.35l-2.15-2.15V14h1v2.79l1.85,1.85 L18.65,19.35z M18,3h-3.18C14.4,1.84,13.3,1,12,1S9.6,1.84,9.18,3H6C4.9,3,4,3.9,4,5v15c0,1.1,0.9,2,2,2h6.11 c-0.59-0.57-1.07-1.25-1.42-2H6V5h2v3h8V5h2v5.08c0.71,0.1,1.38,0.31,2,0.6V5C20,3.9,19.1,3,18,3z M12,5c-0.55,0-1-0.45-1-1 c0-0.55,0.45-1,1-1c0.55,0,1,0.45,1,1C13,4.55,12.55,5,12,5z"/></g></svg>
Mark as incomplete
</button>
<span class="tag">Completed</span>
</div>
<h4 class="task-title">${title}</h4>
<div class="flex align-center gap-0-3 flex-wrap">
${assignedInternsCards}
</div>
<collapsed-text>
${taskDescription}
${branchesButtons.length ? html`<div class="task__branch_container">${branchesButtons}</div>` : ''}
<div class="display-task__details flex flex-wrap gap-0-3 margin-top-1">
<div class="display-task__detail">
<span class="display-task__detail__title">Category: </span>
<span class="display-task__detail__value">${floGlobals.taskCategories[category]}</span>
</div>
<div class="display-task__detail">
<span class="display-task__detail__title">Duration: </span>
<span class="display-task__detail__value">${duration} ${durationType}</span>
</div>
<div class="display-task__detail">
<span class="display-task__detail__title">Max slots: </span>
<span class="display-task__detail__value">${maxSlots}</span>
</div>
<div class="display-task__detail">
<span class="display-task__detail__title">Reward: </span>
<span class="display-task__detail__value">${reward}</span>
</div>
</div>
</collapsed-text>
</li>
`;
}
},
taskRequestCard(request) {
const { details: { taskId, name, brief, contact, portfolioLink }, floID, vectorClock } = request
const internName = RIBC.getInternList()[floID];
const { category } = RIBC.getTaskDetails(taskId);
return html`
<li class="request-card" .dataset=${{ vectorClock, type: 'task' }}>
<div class="flex align-center space-between">
<div class="display-task__category justify-self-start">${floGlobals.taskCategories[category]}</div>
<time>${getFormattedTime(vectorClock.split('_')[0])}</time>
</div>
<p class="request-card__description">
<b>${internName || name}</b> applied for
<b>${RIBC.getTaskDetails(taskId).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(taskId) {
const [projectCode, branch, task] = taskId.split('_');
const { title, description, duration, durationType } = RIBC.getTaskDetails(taskId);
const projectName = RIBC.getProjectDetails(projectCode).projectName;
// per-intern state
const { assignedTasks, completedTasks, failedTasks } = RIBC.getInternRecord(floGlobals.myFloID);
const isCompleted = !!completedTasks?.[taskId];
const isFailed = !!failedTasks?.[taskId];
const showUpdateButton = !isCompleted && !isFailed;
const linkifyDescription = createElement('p', {
innerHTML: DOMPurify.sanitize(linkify(description)),
className: `timeline-task__description ws-pre-line wrap-around`
});
// existing deadline/progress logic (used only for active/overdue)
const { hasDeadlinePassed, taskDeadline, elapsedPercentage } = getTaskDeadline(taskId);
// status/timeline block
const timelineBlock = isCompleted
? html`
<div class="task__completion-timeline flex align-center">
<span class="assigned-intern assigned-intern--completed">
<span class="task-status task-status--completed">Completed</span>
</span>
</div>
`
: (isFailed
? html`
<div class="task__completion-timeline flex align-center">
<span class="assigned-intern assigned-intern--failed">
<span class="task-status task-status--failed">Failed</span>
</span>
</div>
`
: html`
<div class=${`task__completion-timeline flex align-center ${hasDeadlinePassed ? 'deadline-passed' : ''}`}>
${hasDeadlinePassed ? html`
<h3>Overdue</h3>
` : html`
<div class="flex flex-direction-column gap-0-3">
<div class="flex align-center gap-0-3">
<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="M12.36 6l.4 2H18v6h-3.36l-.4-2H7V6h5.36M14 4H5v17h2v-7h5.6l.4 2h7V6h-5.6L14 4z"/></svg>
<span style="font-size: 0.8rem; white-space: nowrap">Assigned</span>
</div>
<time style="font-size: 0.9rem;font-weight: 500; white-space: nowrap">
${getFormattedTime(assignedTasks?.[taskId]?.assignedOn, 'date-only')}
</time>
</div>
<div class="task__completion-timeline__progress" role="progressbar">
<div class="task__completion-timeline__progress__bar" style=${`--progress: ${elapsedPercentage}%`}></div>
<div class="task__completion-timeline__progress__disc"></div>
</div>
<div class="flex flex-direction-column gap-0-3">
<div class="flex align-center gap-0-3">
<svg 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><path d="M0,0h24v24H0V0z" fill="none"/></g><g><g><path d="M11,6H9V4h2V6z M15,4h-2v2h2V4z M9,14h2v-2H9V14z M19,10V8h-2v2H19z M19,14v-2h-2v2H19z M13,14h2v-2h-2V14z M19,4h-2v2h2 V4z M13,8V6h-2v2H13z M7,10V8h2V6H7V4H5v16h2v-8h2v-2H7z M15,12h2v-2h-2V12z M11,10v2h2v-2H11z M9,8v2h2V8H9z M13,10h2V8h-2V10z M15,6v2h2V6H15z"/></g></g></svg>
<span style="font-size: 0.8rem;">Due</span>
</div>
<time style="font-size: 0.9rem;font-weight: 500; white-space: nowrap">
${getFormattedTime(taskDeadline, 'date-only')}
</time>
</div>
`}
</div>
`);
return html`
<li class="task-card" data-unique-id="${taskId}">
<span class="task__project-title">${projectName}</span>
<h4 class="task__title">${title}</h4>
${timelineBlock}
${linkifyDescription}
${showUpdateButton ? html`
<button class="send-update-button button--small button--colored 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>
`;
},
internProfile(internFloId) {
const { joined, completedTasks = {}, active = true } = RIBC.getInternRecord(internFloId) || {}
const internName = RIBC.getInternList()[internFloId]
const rating = RIBC.getInternRating(internFloId)
let completedTasksCount = 0;
let totalPoints = 0
for (const task in completedTasks) {
completedTasksCount++
totalPoints += completedTasks[task].points
}
const splitName = internName.split(' ')
let initials = splitName[0][0]
if (splitName.length > 1) {
initials += splitName[splitName.length - 1][0]
}
const savedUpdates = new Map()
RIBC.getInternUpdates().map(update => {
const { tag, floID, update: { projectCode, branch, task, link, description } } = update
if (tag && floID === internFloId) {
savedUpdates.set(`${projectCode}_${branch}_${task}`, { link, description })
}
})
// const rewardEarned = Object.keys(completedTasks).reduce((acc, task) => {
// return acc + RIBC.getAllTasks()[task].reward
// }, 0)
effect(() => {
renderElem(getRef('intern_profile'), html`
<div id="intern_profile__left">
<div class="flex flex-direction-column align-items-center gap-1-5">
${userType !== 'admin' && !active ? html` <div id="intern_profile__status">Inactive</div> ` : ''}
<div id="intern_profile__initials" class="intern-card__initials" style=${`--color: var(${getInternColor(internFloId)})`}>${initials}</div>
<div class="flex flex-direction-column align-items-center gap-0-5">
<div class="flex align-center gap-0-5">
<h3 id="intern_profile__name" class="text-center">${internName}</h3>
${userType === "admin" ? html`<button id="edit_intern_name" class="button button--small button--colored" onclick=${toggleInternNameEditing}>Edit</button> ` : ''}
</div>
<sm-copy id="intern_profile__flo_id" value=${internFloId}></sm-copy>
</div>
${joined ? html`<p>Joined on ${getFormattedTime(joined, 'date-only')}</p>` : ''}
</div>
<div id="stats_wrapper">
<div id="intern_rating" class="stat">
<div class="stat__display">
<svg class="stat__circle" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="64" cy="64" r="63.5"/> </svg>
<span class="stat__count">${rating}%</h4>
</div>
<p>Rating</p>
</div>
<div id="intern_complete_tasks" class="stat">
<div class="stat__display">
<svg class="stat__circle" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="64" cy="64" r="63.5"/> </svg>
<span class="stat__count">${completedTasksCount}</h4>
</div>
<p>Task completed</p>
</div>
<div id="intern_points" class="stat">
<div class="stat__display">
<svg class="stat__circle" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="64" cy="64" r="63.5"/> </svg>
<span class="stat__count">${totalPoints}</h4>
</div>
<p>Points earned</p>
</div>
</div>
${userType === "admin" ? html`
<sm-switch class="w-100" onchange=${toggleInternStatus} ?checked=${active}>
<div class="grid margin-right-0-5" slot="left">
<h4>Active</h4>
<p>Toggle to change intern status</p>
</div>
</sm-switch>
` : ''}
${fetchPaymentsState.value === 'done' ? html`
<div class="flex align-center space-between gap-1">
<div class="grid">
<p style="font-size: 0.8rem">Earned</p>
<b class="stat__count" style="font-size:1.2rem;">${formatAmount(floGlobals.payments[internFloId]?.total || 0)}</b>
</div>
${floGlobals.payments[internFloId]?.total ? html`
<a href=${`https://ranchimall.github.io/ribcpayments/#/intern?id=${internFloId}`} target="_blank" class="button button--small button--colored">
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"> </path> </svg>
See payments
</a>
`: ``}
</div>
`: html`
<p class="flex align-center gap-0-5">
<sm-spinner></sm-spinner>
<span>Loading earnings...</span>
</p>
`}
</div>
<div id="intern_profile__right" class="flex flex-direction-column gap-1-5">
<div class="flex align-center space-between gap-1">
<h3>Tasks</h3>
<a href=${`#/updates_page?projectCode=all&internId=${internFloId}`} class="button button--small button--colored">See updates</a>
</div>
<div>
<h4>Assigned</h4>
<div>
${render.assignedInternTasks(internFloId) || html`<p>No currently assigned tasks</p>`}
</div>
</div>
<div>
<div class="flex align-center space-between text-align-right">
<h4>Completed</h4>
</div>
<div>
${render.completedInternTasks(internFloId, savedUpdates) || html`<p>No tasks completed yet</p>`}
</div>
</div>
</div>
`)
})
// <div class="grid">
// <p style="font-size: 0.8rem">Earned</p>
// <b class="stat__count" style="font-size:1rem;">${formatAmount(rewardEarned)}</b>
// </div>
let color = '--green';
if (rating < 50) {
color = '--danger-color'
} else if (rating < 80) {
color = '--orange'
}
setTimeout(() => {
getRef('intern_rating').style = `--progress: ${400 - (rating * 4)}; --rating-color:var(${color})`;
}, 0)
},
dashProject(projectCode) {
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
const completePercent = parseInt((completedTasks / (projectTasks.length || 1)) * 100)
const strokeOffset = 76 - (completePercent * 0.76)
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`
<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>
<svg class="progress-icon" style=${`--progress: ${strokeOffset}`} viewBox="0 0 24 24" height="24" width="24"> <circle cx="12" cy="12" r="12" fill="none" /> </svg>
</div>
<a class="grid gap-0-5 flex-1" href=${`#/project_explorer/project?id=${projectCode}&branch=mainLine`}>
<div class="flex align-center">
<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>
</div>
<span class="project__complete-percent">${completePercent}% complete</span>
</a>
<button class="icon-only pin-project" title=${`${isPinned ? 'Unpin' : 'Pin'} this project`} onclick="pinProject(this)" data-pinned=${isPinned}>${pinIcon}</button>
</div>
`;
},
dashProjects() {
const unpinnedProjects = RIBC.getProjectList().filter(project => !pinnedProjects.includes(project)).reverse().map(project => render.dashProject(project))
const renderedPinned = pinnedProjects.map(project => render.dashProject(project))
return html`
<section id="pinned_project_section" class="w-100">
<h4>Pinned</h4>
<div id="pinned_projects" class="observe-empty-state">${renderedPinned}</div>
<div class="empty-state">
<h4>There are no pinned projects</h4>
<p class="margin-block-0-5">
You can pin projects for easier monitoring by clicking on the 'pin' icon on project card.
</p>
</div>
</section>
${unpinnedProjects.length ? html`
<div id="project_list_container">
<div class="flex align-center space-between margin-bottom-0-5">
<h4>Projects</h4>
<a href="#/project_explorer" class="button button--small open-first-project">All</a>
</div>
<div id="project_list" class="flex flex-direction-column gap-0-3">${unpinnedProjects}</div>
</div>
`: ''}
`;
},
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;
const 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(request.details.taskId)
if (!taskDetails) return;
requestCategories.add(RIBC.getTaskDetails(request.details.taskId).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(taskId)) 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 href=${`${location.hash.split('?')[0]}?taskId=${taskId}`}>${RIBC.getTaskDetails(taskId).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}`)
},
taskDisplayMap() {
const displayedTasks = RIBC.getDisplayedTasks()
const allTasks = RIBC.getAllTasks()
const availableToDisplay = []
for (const taskId in allTasks) {
if (displayedTasks.includes(taskId) || RIBC.getTaskStatus(taskId) === 'completed') continue;
availableToDisplay.push(render.draggableTask(taskId))
}
getRef('all_tasks').innerHTML = '';
getRef('all_tasks').append(...availableToDisplay)
getRef('display_task_map').innerHTML = '';
getRef('display_task_map').append(...displayedTasks.map(taskId => render.draggableTask(taskId)))
},
draggableTask(taskId) {
const [projectCode, branch, task] = taskId.split('_')
return html.node`
<li class="flex gap-0-5 displayable-task draggable" data-task-id=${taskId}>
<div class="dragging-handle">
<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="M11 18c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm-2-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 4c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
</div>
<p class="displayable-task__project">${RIBC.getProjectDetails(projectCode).projectName}</p>
<h4>${RIBC.getTaskDetails(taskId).title}</h4>
</li>
`;
},
settings() {
return html`
<div class="grid gap-1-5">
<div class="grid gap-0-5">
<h4>
BTC integrated with FLO
</h4>
<p>
You can use your FLO private key to perform transactions on the BTC network within our
app
ecosystem. The private key is the same for both.
</p>
</div>
<div class="grid gap-0-5">
<b>My FLO address</b>
<sm-copy class="user-flo-id" clip-text value=${floGlobals.myFloID}></sm-copy>
</div>
<div class="grid gap-0-5">
<b>My Bitcoin address</b>
<sm-copy class="user-btc-id" clip-text value=${floGlobals.myBtcID}></sm-copy>
</div>
<button class="button button--danger justify-self-start" onclick="signOut()">Sign out</button>
</div>
${!floGlobals.isPrivKeySecured ? html`
<div class="grid gap-1">
<div class="grid gap-0-5">
<h4>Secure private key</h4>
<p>
You can set a password to secure your private key and use the password instead of private key. This is applied to this browser only.
</p>
</div>
<button id="secure_pwd_button" class=${`button justify-self-start secure-priv-key`} onclick="openPopup('secure_pwd_popup')">Set password</button>
</div>
`: ''}
`;
}
}
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]
}
floGlobals.adminChanges = 0;
const beforeUnloadListener = (event) => {
event.preventDefault();
return event.returnValue = "Are you sure you want to exit?";
};
function adminDataChanged() {
floGlobals.adminChanges++;
getRef('commit_changes_button').setAttribute('data-badge', floGlobals.adminChanges)
if (floGlobals.adminChanges === 1) {
addEventListener("beforeunload", beforeUnloadListener, { capture: true });
getRef('commit_wrapper').classList.remove('hidden')
}
}
function getTaskDeadline(taskId, internId = floGlobals.myFloID) {
const [projectCode, branch, task] = taskId.split('_');
const { title, description, duration, durationType } = RIBC.getTaskDetails(taskId)
const { assignedTasks } = RIBC.getInternRecord(internId)
const assignedOn = assignedTasks[taskId].assignedOn || assignedTasks[taskId]
const durationMilliseconds = durationType === 'days' ? duration * 24 * 60 * 60 * 1000 : duration * 60 * 60 * 1000
const taskDeadline = assignedOn + durationMilliseconds
const elapsedPercentage = Math.round((Date.now() - assignedOn) / durationMilliseconds * 100)
const hasDeadlinePassed = Date.now() > taskDeadline
return {
taskDeadline,
elapsedPercentage,
hasDeadlinePassed
}
}
const filterTasks = debounce((e) => {
const searchQuery = getRef('task_search_input')?.value.trim() || '';
const category = getRef('task_category_selector')?.value || 'all';
window.location.hash = `${location.hash.split('?')[0]}?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(taskId)
const assignedInterns = RIBC.getAssignedInterns(taskId);
let hasApplied = false
try {
floDapps.user.id
hasApplied = floGlobals.isSubAdmin || [...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 - assignedInterns.length}</span>
</div>
`: ''}
${reward ? html`
<div class="display-task__detail">
<span class="display-task__detail__title">Reward: </span>
<span class="display-task__detail__value">₹${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, location.hash.split('?')[0]);
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 userType = 'general';
floGlobals.dashboardPages = {
'intern_view': 'my_tasks',
'dashboard_tasks_wrapper': 'all_tasks',
'projects_wrapper': 'projects',
'intern_leaderboard_container': 'leaderboard',
'active_tasks_wrapper': 'active_tasks',
'certificates_view': 'my_certificates',
}
// find key of the object by value
function getKeyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
}
function handleDashboardViewChange(e) {
location.hash = `#/${appState.currentPage}/${floGlobals.dashboardPages[e.target.value]}`
}
function changeDashboardView(viewId = 'dashboard_tasks_wrapper') {
document.querySelectorAll('.dashboard-view__item').forEach(item => {
if (item.id === 'intern_leaderboard_container')
item.classList.add('hide-on-mobile')
else
item.classList.add('hidden')
})
document.querySelector(`#${viewId}`).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)) {
closePopup();
render.adminInterns();
notify(`${internName} added as an intern.`, 'success')
adminDataChanged();
}
}
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();
adminDataChanged();
}
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)
adminDataChanged();
} else {
e.target.innerHTML = floGlobals.tempEditableContent
}
}
})
function getDaysTaken(start, end = Date.now()) {
const timeTaken = new Date(end) - new Date(start);
const days = Math.floor(timeTaken / (1000 * 60 * 60 * 24));
return days;
}
// 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.adminTask(task)))
} 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) => {
location.hash = `#/${appState.currentPage}/${e.target.value}`
getRef('admin_page_nav_button').href = `#/${appState.currentPage}/${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 })
}
function initTaskScoring(e) {
currentTask = e.target.closest('.admin-task');
renderInternRatingUI()
openPopup('rate_participants_popup')
}
function renderInternRatingUI() {
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
const assignedInterns = RIBC.getAssignedInterns(taskId);
const completionPoints = 30;
const taskScoreElems = assignedInterns.map((internId, index) => {
const { completedTasks, failedTasks } = RIBC.getInternRecord(internId)
if (completedTasks[taskId] || failedTasks[taskId]) {
return html`<div class="flex flex-direction-column gap-0-3" data-intern-id=${internId}>
<h3>${RIBC.getInternList()[internId]}</h3>
<div class="flex align-center space-between">
<p>${completedTasks[taskId] ? 'Task completed' : 'Failed to complete'}</p>
${completedTasks[taskId] ? html`
<div class="flex align-center gap-0-3">
<b>${completedTasks[taskId].points}</b>
<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> <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"></path> </svg>
</div>
` : ''}
</div>
</div>`
} else {
const { duration, durationType } = RIBC.getAllTasks()[taskId];
const deadlineDays = durationType === 'days' ? duration : duration * 30;
const daysTaken = getDaysTaken(RIBC.getInternRecord(internId).assignedTasks[taskId].assignedOn)
let quicknessPoints = 0;
if (daysTaken < deadlineDays * 0.7) {
quicknessPoints = 30
} else if (daysTaken < deadlineDays * 0.8) {
quicknessPoints = 25
} else if (daysTaken < deadlineDays) {
quicknessPoints = 20
}
return html`
<div class="flex flex-direction-column gap-0-5 rating-part" data-intern-id=${internId}>
<h3>${RIBC.getInternList()[internId]}</h3>
<sm-form class="grid gap-0-5">
<div class="flex gap-0-5">
<sm-input value=${completionPoints + quicknessPoints} type="number" min="0" max="60" class="flex-1 automated-points" placeholder="Automated" error-text="Points must be between 0-60" ?autofocus=${index === 0} animate required></sm-input>
<sm-input type="number" min="0" max="40" class="flex-1 subjective-points" placeholder="Subjective" error-text="Points must be between 0-40" animate required></sm-input>
</div>
<div class="flex gap-0-5">
<input class="flex-1" type="date" value=${formatDate(new Date())} placeholder="Completion date" aria-label="Set date of completion" required>
<button class="button button--primary rate-intern-button" type="submit">Rate</button>
</div>
</sm-form>
</div>
`;
}
})
renderElem(getRef('rating_wrapper'), html`
<h4>${currentTask.querySelector('.task-title').textContent}</h4>
<sm-form>
${taskScoreElems}
</sm-form>
`)
}
//Fixing the earlier incomplete marking
function markTaskAsIncomplete(e) {
const card = e?.target?.closest?.('.admin-task');
if (!card) return;
currentTask = card;
getConfirmation('Mark this task as incomplete?', {
message: 'Any completion scores for this task will be removed from intern ratings.',
confirmText: 'Mark as incomplete',
danger: true
}).then(res => {
if (!res) return;
// 1) Source of truth
RIBC.admin.putTaskStatus('incomplete', appState.params.id, appState.params.branch, currentTask.dataset.taskId);
// Full task id
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
// 3) Who was assigned?
const getAssigned = RIBC.getAssignedInterns || RIBC.admin?.getAssignedInterns;
const assignedInterns = (typeof getAssigned === 'function' ? getAssigned(taskId) : []) || [];
const reopenedDate = Date.now();
// 4) Append-only reopen + remove completion + recompute rating
assignedInterns.forEach(internId => {
RIBC.admin.addReopenedTask(internId, taskId, { reopenedDate });
RIBC.admin.removeCompletedTask?.(internId, taskId);
RIBC.admin.recomputeRating?.(internId);
});
// 5) Ensure it appears back in the admin's "displayed" list as pending
const shown = new Set(RIBC.getDisplayedTasks?.() || []);
shown.add(taskId);
RIBC.admin.setDisplayedTasks?.(Array.from(shown));
// (optional safety) keep internRecord/taskStatus fully consistent
RIBC.admin.syncInternRecordWithTaskStatus?.(taskId);
renderBranchTasks();
adminDataChanged();
notify('Task marked as incomplete', 'success');
});
}
delegate(getRef('rating_wrapper'), 'click', '.rate-intern-button', e => {
const ratingPart = e.target.closest('.rating-part');
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
const internId = ratingPart.dataset.internId;
const automatedPoints = parseFloat(ratingPart.querySelector('.automated-points').value.trim()) || 0;
const subjectivePoints = parseFloat(ratingPart.querySelector('.subjective-points').value.trim()) || 0;
const points = automatedPoints + subjectivePoints;
const completionDate = new Date(ratingPart.querySelector('input').value).getTime();
RIBC.admin.addCompletedTask(internId, taskId, points, { completionDate })
notify('Task score added', 'success')
adminDataChanged();
renderInternRatingUI()
})
// format unix timestamp to yyyy-mm-dd
function formatDate(unixTimestamp) {
const date = new Date(unixTimestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`;
}
function markTaskAsCompleted(e) {
const card = e?.target?.closest?.('.admin-task');
if (!card) return;
currentTask = card;
getConfirmation('Mark this task as completed?', { confirmText: 'Mark as completed' })
.then(res => {
if (!res) return;
// 1) Source of truth
RIBC.admin.putTaskStatus('completed', appState.params.id, appState.params.branch, currentTask.dataset.taskId);
// 2) Full task id
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
// 3) Who was assigned?
const getAssigned = RIBC.getAssignedInterns || RIBC.admin?.getAssignedInterns;
const assignedInterns = (typeof getAssigned === 'function' ? getAssigned(taskId) : []) || [];
const completionDate = Date.now();
// 4) Record completion for each intern + recompute rating
assignedInterns.forEach(internId => {
RIBC.admin.addCompletedTask(internId, taskId, 0, { completionDate });
RIBC.admin.recomputeRating?.(internId);
});
// 5) Remove from displayed "pending" list
const filteredTasks = (RIBC.getDisplayedTasks?.() || []).filter(t => t !== taskId);
RIBC.admin.setDisplayedTasks?.(filteredTasks);
// (optional safety) keep internRecord/taskStatus fully consistent
RIBC.admin.syncInternRecordWithTaskStatus?.(taskId);
renderBranchTasks();
adminDataChanged();
notify('Task marked as completed', 'success');
});
}
function saveTaskChanges() {
const changedDetails = {
title: getRef('edit_task_title').value.trim(),
description: DOMPurify.sanitize(getRef('edit_task_description').innerHTML.trim()),
category: getRef('edit_task_category').value,
duration: parseInt(getRef('edit_task_duration').value),
durationType: getRef('edit_task_duration_type').value,
maxSlots: parseInt(getRef('edit_task_max_slots').value),
reward: parseInt(getRef('edit_task_reward').value)
}
const ogTaskDetails = RIBC.getTaskDetails(`${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`)
const changedKeys = Object.keys(changedDetails).filter(key => ogTaskDetails[key] !== changedDetails[key])
if (changedKeys.length) {
RIBC.admin.editTaskDetails(changedDetails, appState.params.id, appState.params.branch, currentTask.dataset.taskId)
renderBranchTasks();
notify('Changes saved locally, commit the changes to make them permanent', 'success')
adminDataChanged();
} else {
notify('No changes detected')
}
closePopup();
}
getRef('task_list').addEventListener('click', (e) => {
if (e.target.closest('.admin-task')) {
currentTask = e.target.closest('.admin-task');
}
if (e.target.closest('.unassign-intern-button')) {
const internCard = e.target.closest('.assigned-intern')
const internId = internCard.dataset.floId
const contentMenu = html.node`
<ul class="menu" data-flo-id=${internId}>
<li class="menu__item interactive">
<button onclick=${markAsFailed}>
<svg class="icon margin-right-1" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M14.59 8L12 10.59 9.41 8 8 9.41 10.59 12 8 14.59 9.41 16 12 13.41 14.59 16 16 14.59 13.41 12 16 9.41 14.59 8zM12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
Mark as failed
</button>
</li>
<li class="menu__item interactive">
<button onclick=${unassignIntern}>
<svg class="icon margin-right-1" 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><path d="M14,8c0-2.21-1.79-4-4-4C7.79,4,6,5.79,6,8c0,2.21,1.79,4,4,4C12.21,12,14,10.21,14,8z M12,8c0,1.1-0.9,2-2,2 c-1.1,0-2-0.9-2-2s0.9-2,2-2C11.1,6,12,6.9,12,8z"/><path d="M2,18v2h16v-2c0-2.66-5.33-4-8-4C7.33,14,2,15.34,2,18z M4,18c0.2-0.71,3.3-2,6-2c2.69,0,5.77,1.28,6,2H4z"/><rect height="2" width="6" x="17" y="10"/></g></g></svg>
Unassign
</button>
</li>
</ul>
`;
internCard.appendChild(contentMenu)
contentMenu.animate(slideInDown, {
duration: floGlobals.prefersReducedMotion ? 0 : 200,
easing: 'ease'
})
.onfinish = () => {
contentMenu.querySelector('button').focus()
document.addEventListener("click", function (e) {
contentMenu.animate(slideOutUp, {
duration: floGlobals.prefersReducedMotion ? 0 : 100,
easing: 'ease'
}).onfinish = () => {
contentMenu.remove()
}
}, { once: true });
}
}
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)
RIBC.admin.setDisplayedTasks([`${appState.params.id}_${appState.params.branch}_${task}`, ...RIBC.getDisplayedTasks()])
card.remove()
renderBranchTasks()
getRef('add_task').classList.remove('hidden')
notify('Task added to current branch', 'success')
adminDataChanged();
}
})
function markAsFailed(e) {
getConfirmation('Failed to complete task?', { message: `This will unassign intern and mark this task as failed in their record.`, confirmText: 'Mark as failed', danger: true }).then((result) => {
if (result) {
const internId = e.target.closest('.menu').dataset.floId
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`
const done = RIBC.admin.addFailedTask(internId, taskId)
if (done) {
notify('Task marked as failed', 'success')
renderBranchTasks()
adminDataChanged();
} else {
notify('Failed to mark task as failed', 'error')
}
}
})
}
function unassignIntern(e) {
getConfirmation('Unassign intern from task?', { message: `This will remove record of intern from task data.`, confirmText: 'Unassign', danger: true }).then((result) => {
if (result) {
RIBC.admin.unassignInternFromTask(e.target.closest('.menu').dataset.floId, `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`)
notify('Intern removed from the task')
renderBranchTasks()
adminDataChanged();
}
})
}
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?", { confirmText: 'Save' }).then((result) => {
if (result) {
RIBC.admin.updateObjects().then(res => {
notify('Changes committed.', 'success')
floGlobals.adminChanges = 0
getRef('commit_changes_button').removeAttribute('data-badge')
removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
getRef('commit_wrapper').classList.add('hidden')
}).catch(err => {
console.error(err)
})
}
})
}
function removeThisTask() {
getConfirmation("Are you sure to delete this task?", { confirmText: 'Delete', danger: true }).then((result) => {
if (result) {
RIBC.admin.deleteTaskInMap(appState.params.id, appState.params.branch, currentTask.dataset.taskId)
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
RIBC.admin.setDisplayedTasks(RIBC.getDisplayedTasks().filter(task => task !== taskId))
renderBranchTasks()
adminDataChanged();
}
})
}
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()
adminDataChanged();
}
function renderAllInterns() {
renderElem(getRef('all_interns_list'), filterInterns('', { sortByRating: true }))
}
function openNewBranchPopup() {
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()
adminDataChanged();
}
function openTaskEditingPopup(e) {
const taskNo = e.target.closest('.admin-task').dataset.taskId
const { title, description, category, maxSlots, duration, durationType, reward } = RIBC.getTaskDetails(`${appState.params.id}_${appState.params.branch}_${taskNo}`)
if (!getRef('edit_task_category').firstElementChild) {
const categories = objMap(floGlobals.taskCategories, (categoryName, categoryID) => {
return html`<sm-option value=${categoryID} ?selected=${category === categoryID}>${categoryName}</sm-option>`
})
renderElem(getRef('edit_task_category'), html`${categories}`)
}
getRef('edit_task_title').value = title
getRef('edit_task_description').innerHTML = DOMPurify.sanitize(description)
getRef('edit_task_category').value = category
getRef('edit_task_duration').value = duration
getRef('edit_task_duration_type').value = durationType
getRef('edit_task_max_slots').value = maxSlots
getRef('edit_task_reward').value = reward
openPopup('task_editing_popup')
getRef('task_edit_form')._checkValidity()
}
function toggleInternNameEditing(e) {
const button = e.target.closest('button');
if (getRef('intern_profile__name').isContentEditable) {
const floId = appState.params.id;
const newName = getRef('intern_profile__name').textContent.trim();
if (newName !== '' && floGlobals.tempEditableContent !== newName) {
RIBC.admin.renameIntern(floId, newName)
notify('Intern name updated locally, please commit changes to make them permanent.', 'success')
}
getRef('intern_profile__name').contentEditable = false;
button.textContent = 'Edit';
document.getSelection().collapseToEnd()
floGlobals.tempEditableContent = '';
adminDataChanged();
} else {
makeEditable(getRef('intern_profile__name'))
button.textContent = 'Done'
}
}
function toggleInternStatus(e) {
const floId = appState.params.id;
RIBC.admin.setInternStatus(floId, e.target.value)
notify('Intern status updated locally, please commit changes to make them permanent.', 'success')
adminDataChanged();
}
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.parentNode.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.parentNode.classList.add('hidden')
e.target.closest('.intern-update').querySelector('.update-reply-textarea').focusIn()
})
delegate(getRef('all_updates_list'), 'click', '.save-update', (e) => {
const vectorClock = e.delegateTarget.closest('.intern-update').dataset.vectorClock;
getConfirmation('Are you sure you want to save this update?', { confirmText: 'Save' }).then((res) => {
if (res) {
floCloudAPI.tagApplicationData(vectorClock, 'saved').then(() => {
notify('Update saved', 'success')
e.delegateTarget.remove()
}).catch(() => {
notify('Failed to save update', 'error')
})
}
})
})
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(`${floGlobals.myFloID}_pinned_projects`).split(',') : []
if (pinnedProjects.includes(projectCode)) {
pinnedProjects = pinnedProjects.filter(project => project !== projectCode)
} else {
pinnedProjects.push(projectCode)
}
localStorage.setItem(`${floGlobals.myFloID}_pinned_projects`, pinnedProjects.join())
renderElem(getRef('projects_wrapper'), render.dashProjects())
}
let sessionTaskRequests = new Set();
function requestForTask(btn) {
hideTaskDetails()
try {
floDapps.user.id
const taskId = btn ? btn.dataset.taskId : floGlobals.tempUserTaskRequest
floGlobals.tempUserTaskRequest = taskId
if (userType === 'general') {
getRef('intern_apply__task').textContent = RIBC.getAllTasks()[taskId].title
openPopup('apply_for_task_popup', true)
} else if (userType === '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 {
const { assignedTasks, completedTasks, failedTasks } = RIBC.getInternRecord(floGlobals.myFloID)
if (assignedTasks[taskId])
return notify('You have already been assigned this task', 'error');
else if (completedTasks[taskId])
return notify('You have already completed this task', 'error');
else if (failedTasks[taskId])
return notify('You have already failed this task', 'error');
const { title } = RIBC.getTaskDetails(taskId)
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')
}
document.addEventListener('popupopened', e => {
getRef('main_page').setAttribute('inert', '')
switch (e.target.id) {
case 'intern_list_popup':
renderElem(getRef('intern_list_container'), filterInterns('', { availableInternsOnly: true, activeOnly: true }))
break;
case 'issue_certificate_popup':
async function issueCertificate() {
const floId = getRef('issue_certificate__intern_selector').value;
const name = RIBC.getInternList()[floId];
const certificateType = getRef('issue_certificate__certificate_selector').value;
const description = getRef('issue_certificate__description').value.trim();
const privateKey = getRef('issue_certificate__private_key').value.trim();
const isCorrectPrivateKey = floCrypto.verifyPrivKey(privateKey, floGlobals.certificateIssuerAddress)
if (!isCorrectPrivateKey) {
notify('Invalid private key', 'error')
return;
}
const receiver = certificateType === 'CERTIFICATE OF EMPLOYMENT' ? floGlobals.rmIncorporationId : floGlobals.ribcId;
const consent = await getConfirmation(`Issue certificate to ${name}?`, { confirmText: 'Issue' })
if (!consent) return;
buttonLoader(getRef('issue_button'), true)
floBlockchainAPI.writeData(floGlobals.certificateIssuerAddress, `${certificateType}|${floId}|${name}|${description}`, privateKey, receiver).then((txid) => {
console.log(txid)
renderElem(getRef('issue_certificate_popup__content'), html`
<div class="grid text-center gap-1-5 justify-items-center margin-auto">
<div class="grid text-center gap-0-5">
<h2>Certificate issued</h2>
<p>
Will be listed after confirmation on the blockchain.
</p>
</div>
<a href="${floBlockchainAPI.apiURL.replace(/\/$/, '')}/tx/${txid}" target="_blank" class="button button--primary">View on explorer</a>
</div>
`)
}).catch((error) => {
notify(error, 'error')
}).finally(() => {
buttonLoader(getRef('issue_button'), false)
})
}
renderElem(getRef('issue_certificate_popup__content'), html`
<sm-form class="grid gap-0-5">
<div class="grid">
<p>Issue to</p>
<sm-select id="issue_certificate__intern_selector" required>
${Object.entries(RIBC.getInternList()).sort((a, b) => a[1].localeCompare(b[1])).map(([floId, name]) => html`<sm-option value=${floId}>${name}</sm-option>`)}
</sm-select>
</div>
<div class="grid">
<p>Certificate type</p>
<sm-select id="issue_certificate__certificate_selector" required>
<sm-option value="CERTIFICATE OF INTERNSHIP">Certificate of Internship</sm-option>
<sm-option value="CERTIFICATE OF EMPLOYMENT">Certificate of Employment</sm-option>
<sm-option value="CERTIFICATE OF VOLUNTEERSHIP">Certificate of Volunteership</sm-option>
<sm-option value="CERTIFICATE OF PARTICIPATION">Certificate of Participation</sm-option>
</sm-select>
</div>
<div class="grid">
<p>Description</p>
<sm-textarea id="issue_certificate__description" rows="6" required></sm-textarea>
</div>
<div class="grid">
<p>Private key of ${floGlobals.certificateIssuerAddress}</p>
<sm-input id="issue_certificate__private_key" type="password" required></sm-input>
</div>
<div class="multi-state-button">
<button id="issue_button" class="button button--primary" type="submit" onclick=${issueCertificate} disabled>Issue</button>
</div>
</sm-form>
`)
break;
case 'profile_popup':
renderElem(getRef('profile_popup__content'), render.settings())
break;
}
})
document.addEventListener('popupclosed', e => {
switch (e.target.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;
case 'rate_participants_popup':
renderElem(getRef('rating_wrapper'), html``)
break;
}
if (popupStack.items.length === 0) {
getRef('main_page').removeAttribute('inert')
}
zIndex--;
})
function renderAllElements() {
let sortedProjectList = getSortedProjectList()
document.querySelectorAll('.open-first-project').forEach(link => {
const adminPage = link.id === 'admin_page_nav_button'
link.href = adminPage ? `${link.href}/projects?id=${sortedProjectList[0]}&branch=mainLine` : `${link.href}/project?id=${sortedProjectList[0]}&branch=mainLine`
})
pinnedProjects = localStorage.getItem(`${floGlobals.myFloID}_pinned_projects`) ? localStorage.getItem(`${floGlobals.myFloID}_pinned_projects`).split(',') : []
// Intern's view
if (RIBC.getInternList()[floGlobals.myFloID] && !floGlobals.subAdmins.includes(floGlobals.myFloID)) {
userType = 'intern';
document.querySelectorAll('.intern-option').forEach((option) => {
option.classList.remove('hidden')
})
} else {
document.querySelectorAll('.intern-option').forEach((option) => {
option.classList.add('hidden')
})
}
// admin view
if (floGlobals.subAdmins.includes(floGlobals.myFloID)) {
userType = 'admin'
document.querySelectorAll('.admin-option').forEach((option) => {
option.classList.remove('hidden')
})
} else {
document.querySelectorAll('.admin-option').forEach((option) => {
option.classList.add('hidden')
})
}
// General only view for non admin and non intern
if (!RIBC.getInternList()[floGlobals.myFloID] && !floGlobals.subAdmins.includes(floGlobals.myFloID)) {
document.querySelectorAll('.general-only').forEach((elem) => {
elem.classList.remove('hidden')
})
}
else {
document.querySelectorAll('.general-only').forEach((elem) => {
elem.classList.add('hidden')
})
}
if (userType === "admin") {
document.querySelectorAll('.not-for-admin').forEach((elem) => {
elem.classList.add('hidden')
})
} else {
document.querySelectorAll('.not-for-admin').forEach((elem) => {
elem.classList.remove('hidden')
})
}
render.projectList(getRef('all_projects'), sortedProjectList)
delegate(getRef('explorer_task_list'), 'click', '.apply-button', e => {
requestForTask(e.delegateTarget)
})
}
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(currentTaskId).title
openPopup('post_update_popup')
}
function postUpdate() {
const [projectCode, branch, task] = currentTaskId.split('_')
const description = getRef('update__brief').value.trim()
if (description !== '') {
buttonLoader(getRef('post_update_btn'), true)
RIBC.postInternUpdate({ projectCode, branch, task, description })
.then((result) => {
notify('Update posted', 'success')
closePopup()
}).catch((error) => {
notify(error, 'error')
}).finally(() => {
buttonLoader(getRef('post_update_btn'), false)
})
} else {
notify('Please enter description', 'error')
}
}
function filterInterns(searchKey, options = {}) {
const {
sortByRating = false,
availableInternsOnly = false,
activeOnly = false,
limit = undefined,
sortAlphabetically = false
} = options
let filtered = [];
let arrayOfInterns = [];
const allInterns = RIBC.getInternList();
if (sortByRating)
arrayOfInterns = Object.keys(allInterns).sort((a, b) => {
return RIBC.getInternRating(b) - RIBC.getInternRating(a)
})
else if (sortAlphabetically)
arrayOfInterns = Object.keys(allInterns).sort((a, b) => {
return allInterns[a].toLowerCase().localeCompare(allInterns[b].toLowerCase())
})
else
arrayOfInterns = Object.keys(allInterns)
if (availableInternsOnly) {
arrayOfInterns = arrayOfInterns.filter(intern => !RIBC.getAssignedInterns(`${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`)?.includes(intern))
}
if (activeOnly) {
arrayOfInterns = arrayOfInterns.filter(intern => {
return RIBC.getInternRecord(intern).active
})
}
if (limit) {
arrayOfInterns = arrayOfInterns.slice(0, limit)
}
if (searchKey === '') {
filtered = 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, activeOnly: 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 portfolioLink = getRef('intern_apply__portfolio_link').value.trim();
const details = {
name,
brief,
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
} 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;
}
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('keys_generator').keys.privKey);
getRef('keys_generator').clearKeys();
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');
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) {
if (!inputText)
return ''
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;
}
// return string between two strings
function getBetween(str, start, end) {
return str.substring(str.indexOf(start) + start.length, str.indexOf(end));
}
// get first three words of a string
function getFirstThreeWords(str) {
return str.split(' ').slice(0, 3).join(' ')
}
// get all words after given word including the word
function removeWordsBefore(str, word) {
return str.substring(str.indexOf(word));
}
floGlobals.validCerts = new Map();
let fetchCertificateIssueTxsState = signal('idle');
function fetchCertificateIssueTxs() {
return new Promise((resolve, reject) => {
fetchCertificateIssueTxsState.value = 'fetching'
floBlockchainAPI.readData(floGlobals.certificateIssuerAddress, {
sentOnly: true,
tx: true,
receivers: [floGlobals.rmIncorporationId, floGlobals.ribcId],
filter: d => d?.startsWith('CERTIFICATE OF')
}).then(res => {
for (const tx of res.items) {
const { time, txid, blockheight } = tx;
const floData = tx.data;
let name, floId, certType, certPara = ''
// check if certificates are of newer format
if (blockheight > 5787555) {
[certType, floId, name, certPara] = floData.split('|')
} else {
floId = floData.match(/\b\w{30,36}\b/)?.[0].trim()
name = getBetween(floData, 'certifies that', 'FLO ID').replace(/(,|\s)+$/, '')
certType = getFirstThreeWords(floData).trim()
}
if (!floId) continue;
let certificateVerification = ''
switch (certType) {
case "CERTIFICATE OF INTERNSHIP":
certificateVerification = 'internCertificate';
break;
case "CERTIFICATE OF EMPLOYMENT":
certificateVerification = 'employeeCertificate';
break;
case "CERTIFICATE OF VOLUNTEERSHIP":
certificateVerification = 'volunteerCertificate';
break;
case "CERTIFICATE OF PARTICIPATION":
certificateVerification = 'participationCertificate';
break;
}
const verificationLink = `https://www.ranchimall.net/verify/?${certificateVerification}=${txid}`
const btcId = btcOperator.convert.legacy2bech(floId)
floGlobals.validCerts.set(floId, { txid, name, floId, btcId, certType, certPara, verificationLink })
}
resolve()
fetchCertificateIssueTxsState.value = 'done'
}).catch(err => {
notify(err, 'error')
reject(err)
fetchCertificateIssueTxsState.value = 'idle'
})
})
}
floGlobals.payments = {};
floGlobals.payer = "FThgnJLcuStugLc24FJQggmp2WgaZjrBSn";
let fetchPaymentsState = signal('idle');
// ---- helpers (put once, top-level) ----
function getReceiverAddress(vout, payer) {
if (!Array.isArray(vout)) return undefined;
for (const o of vout) {
const addrs = o?.scriptPubKey?.addresses || [];
for (const a of addrs) {
if (a && a !== payer) return a; // first non-payer output = payee
}
}
return undefined;
}
function getInputAddresses(tx) {
const outs = [];
for (const vin of (tx?.vin || [])) {
const addrs = vin?.addresses || (vin?.addr ? [vin.addr] : []);
for (const a of addrs) if (a) outs.push(a);
}
return outs;
}
// strict: require ALL inputs come from the payer/internship distribution address
function isFromPayer(tx, payer) {
const ins = getInputAddresses(tx);
return ins.length > 0 && ins.every(a => a === payer);
}
function parseFloAmount(floData) {
// Handles "send 3000 rupee#" or "Send 8000.0000000000 rupee# ..."
const m = /send\s+([\d.]+)\s+[A-Za-z0-9#]+/i.exec(floData || "");
return m ? parseFloat(m[1]) : 0;
}
function isRupeeSend(floData) {
return /send\s+[\d.]+\s+rupee#/i.test(floData || "");
}
function fetchPayments() {
fetchPaymentsState.value = 'fetching'
floBlockchainAPI
.readAllTxs(floGlobals.payer)
.then(({ items }) => {
const internList = RIBC.getInternList() || {};
if (!floGlobals.payments) floGlobals.payments = Object.create(null);
items.forEach((tx) => {
// sender must be the payer/internship distribution address
if (!isFromPayer(tx, floGlobals.payer)) return;
// pick the true receiver (first non-payer vout address)
const floId = getReceiverAddress(tx?.vout, floGlobals.payer);
if (!floId) return;
// only count recognized interns
if (!internList[floId]) return;
// only count rupee# sends
if (!isRupeeSend(tx.floData)) return;
const amount = parseFloAmount(tx.floData);
const txid = tx.txid;
const time = Number(tx.time) || 0;
// init bucket
if (!floGlobals.payments[floId]) {
floGlobals.payments[floId] = { total: 0, txs: [] };
}
// prevent duplicates (in case readAllTxs returns same tx in multiple pages later)
const rec = floGlobals.payments[floId];
if (!rec._seen) rec._seen = new Set();
if (rec._seen.has(txid)) return;
rec._seen.add(txid);
rec.total += amount;
rec.txs.push({ txid, amount, time });
});
// sort each interns payments by time desc (optional)
for (const k in floGlobals.payments) {
floGlobals.payments[k].txs.sort((a, b) => b.time - a.time);
}
fetchPaymentsState.value = 'done';
})
.catch((err) => {
notify(err, 'error');
fetchPaymentsState.value = 'idle';
});
}
</script>
</body>
</htm>