ribc/index.html
2024-02-24 03:18:30 +05:30

4230 lines
271 KiB
HTML

<!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.min.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.min.js" defer></script>
<script src="scripts/floCrypto.min.js" defer></script>
<script src="scripts/floBlockchainAPI.min.js" defer></script>
<script src="scripts/compactIDB.min.js" defer></script>
<script src="scripts/floCloudAPI.min.js" defer></script>
<script src="scripts/floDapps.min.js" defer></script>
<script src="scripts/btcOperator.min.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.min.js" defer></script>
<script id="onLoadStartUp">
function onLoadStartUp() {
routeTo('loading')
document.body.classList.remove('hidden')
floDapps.setCustomPrivKeyInput(getSignedIn);
floDapps.setMidStartup(() =>
new Promise((resolve, reject) => {
RIBC.refreshObjectData()
.then(() => {
resolve()
setTimeout(() => {
if (!floGlobals.loaded) {
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()
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
const { assignedTasks } = RIBC.getInternRecord(floGlobals.myFloID)
const linkifyDescription = createElement('p', {
innerHTML: DOMPurify.sanitize(linkify(description)),
className: `timeline-task__description ws-pre-line wrap-around`
})
const { hasDeadlinePassed, taskDeadline, elapsedPercentage } = getTaskDeadline(taskId)
return html`
<li class="task-card" data-unique-id="${taskId}">
<span class="task__project-title">${projectName}</span>
<h4 class="task__title">${title}</h4>
<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>
${linkifyDescription}
<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>
`)
}
function markTaskAsIncomplete(e) {
currentTask = e.target.closest('.admin-task');
getConfirmation('Mark this task as incomplete?', { message: 'Score given to participants regarding this task will also be removed', confirmText: 'Mark as incomplete', danger: true }).then(res => {
if (res) {
RIBC.admin.putTaskStatus('incomplete', appState.params.id, appState.params.branch, currentTask.dataset.taskId)
// TODO: remove task scores from intern rating
renderBranchTasks()
adminDataChanged();
}
})
}
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() {
getConfirmation('Mark this task as completed?', { confirmText: 'Mark as completed' }).then(res => {
if (res) {
RIBC.admin.putTaskStatus('completed', appState.params.id, appState.params.branch, currentTask.dataset.taskId)
// remove task from displayed list
const taskId = `${appState.params.id}_${appState.params.branch}_${currentTask.dataset.taskId}`;
const filteredTasks = RIBC.getDisplayedTasks().filter(task => task !== taskId)
RIBC.admin.setDisplayedTasks(filteredTasks)
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=${`https://blockbook.ranchimall.net/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 = {};
let fetchPaymentsState = signal('idle');
function fetchPayments() {
fetchPaymentsState.value = 'fetching'
floBlockchainAPI
.readAllTxs("FThgnJLcuStugLc24FJQggmp2WgaZjrBSn")
.then(({ items }) => {
items.forEach((tx) => {
const floId = tx.vout[0].scriptPubKey.addresses[0];
if (!RIBC.getInternList()[floId]) return; // check if floId is of an intern
const { txid, floData, time } = tx
if (!floGlobals.payments[floId])
floGlobals.payments[floId] = {
total: 0,
txs: []
};
const amount = parseFloat(floData.match(/([0-9]+)/)[1]) || 0; // get amount from floData
floGlobals.payments[floId].total += amount;
floGlobals.payments[floId].txs.push({
txid,
amount,
time
});
});
fetchPaymentsState.value = 'done'
}).catch((err) => {
notify(err, 'error')
fetchPaymentsState.value = 'idle'
});
}
</script>
</body>
</htm>