3936 lines
246 KiB
HTML
3936 lines
246 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"
|
|
}
|
|
</script>
|
|
<script src="scripts/lib.js" defer></script>
|
|
<script src="scripts/floCrypto.js" defer></script>
|
|
<script src="scripts/floBlockchainAPI.js" defer></script>
|
|
<script src="scripts/compactIDB.js" defer></script>
|
|
<script src="scripts/floCloudAPI.js" defer></script>
|
|
<script src="scripts/floDapps.js" defer></script>
|
|
<script src="scripts/btcOperator.js" defer></script>
|
|
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js"
|
|
integrity="sha512-/hVAZO5POxCKdZMSLefw30xEVwjm94PAV9ynjskGbIpBvHO9EBplEcdUlBdCKutpZsF+La8Ag4gNrG0gAOn3Ig=="
|
|
crossorigin="anonymous" referrerpolicy="no-referrer" defer></script>
|
|
<script src="scripts/ribc.js" defer></script>
|
|
<script id="onLoadStartUp">
|
|
function onLoadStartUp() {
|
|
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)
|
|
})
|
|
})
|
|
)
|
|
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">
|
|
<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-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>
|
|
</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="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 id="default_ui_library">
|
|
"use strict";
|
|
// Global variables
|
|
const { html, render: renderElem } = uhtml;
|
|
//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.`;
|
|
}
|
|
}
|
|
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>
|
|
${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>
|
|
`)
|
|
changeDashboardView(getKeyByValue(floGlobals.dashboardPages, subPageId1))
|
|
if (params?.taskId) {
|
|
showTaskDetails(params.taskId)
|
|
} else
|
|
hideTaskDetails()
|
|
}
|
|
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 = ['projects', 'interns', 'task_display', 'requests'].findIndex(page => page === subPageId1);
|
|
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;
|
|
}
|
|
}
|
|
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)
|
|
console.log('rewardEarned', rewardEarned)
|
|
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> ` : ''}
|
|
</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',
|
|
}
|
|
// 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 '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) {
|
|
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;
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</htm>
|