diff --git a/constants.js b/constants.js index d4b50532..94fd1b91 100644 --- a/constants.js +++ b/constants.js @@ -1,2 +1,3 @@ const API_BASE_URL = 'https://api.realdevsquad.com'; const USER_MANAGEMENT_LINK = 'user-management-link'; +const EXTENSION_REQUESTS_LINK = 'extension-requests-link'; diff --git a/extension-requests/constants.js b/extension-requests/constants.js new file mode 100644 index 00000000..95f314f1 --- /dev/null +++ b/extension-requests/constants.js @@ -0,0 +1,19 @@ +const taskInfoModelHeadings = [ + { title: 'Title' }, + { title: 'Ends On', key: 'endsOn', time: true }, + { title: 'Purpose' }, + { title: 'Assignee' }, + { title: 'Created By', key: 'createdBy' }, + { title: 'Is Noteworthy', key: 'isNoteworthy' }, +]; + +const extensionRequestCardHeadings = [ + { title: 'Title' }, + { title: 'Reason' }, + { title: 'Old Ends On', key: 'oldEndsOn', time: true }, + { title: 'New Ends On', key: 'newEndsOn', time: true }, + { title: 'Status', bold: true }, + { title: 'Assignee' }, + { title: 'Created At', key: 'timestamp', time: true }, + { title: 'Task', key: 'taskId' }, +]; diff --git a/extension-requests/index.html b/extension-requests/index.html new file mode 100644 index 00000000..9675032c --- /dev/null +++ b/extension-requests/index.html @@ -0,0 +1,110 @@ + + + + + + + Extension Requests + + + +
+
+
+

Update Extension Request

+ + + + + + + + + + + + + + + + +
+
+

Update Extension Request Status

+ + + + + + + + + + +
+
+
+

Extension Requests

+
+
+
+
+

+ + + + + + + diff --git a/extension-requests/local-utils.js b/extension-requests/local-utils.js new file mode 100644 index 00000000..513555ad --- /dev/null +++ b/extension-requests/local-utils.js @@ -0,0 +1,101 @@ +async function getExtensionRequests(query = {}) { + const url = new URL(`${API_BASE_URL}/extension-requests`); + + queryParams = ['assignee', 'status', 'taskId']; + queryParams.forEach( + (key) => query[key] && url.searchParams.set(key, query[key]), + ); + + const res = await fetch(url, { + credentials: 'include', + method: 'GET', + headers: { + 'Content-type': 'application/json', + }, + }); + return await res.json(); +} + +async function updateExtensionRequest({ id, body }) { + const url = `${API_BASE_URL}/extension-requests/${id}`; + const res = await fetch(url, { + credentials: 'include', + method: 'PATCH', + body: JSON.stringify(body), + headers: { + 'Content-type': 'application/json', + }, + }); + return await res.json(); +} + +async function updateExtensionRequestStatus({ id, body }) { + const url = `${API_BASE_URL}/extension-requests/${id}/status`; + const res = await fetch(url, { + credentials: 'include', + method: 'PATCH', + body: JSON.stringify(body), + headers: { + 'Content-type': 'application/json', + }, + }); + return await res.json(); +} + +async function getTaskDetails(taskId) { + if (!taskId) return; + const url = `${API_BASE_URL}/tasks/${taskId}/details`; + const res = await fetch(url, { + credentials: 'include', + method: 'GET', + headers: { + 'Content-type': 'application/json', + }, + }); + return await res.json(); +} + +function getTimeFromTimestamp(timestamp) { + return new Date(timestamp * 1000).toLocaleString(); +} + +function createTable(headings, data, className = '') { + const table = createElement({ + type: 'table', + attributes: { + class: className, + }, + }); + const tableBody = createElement({ type: 'tbody' }); + headings.forEach(({ title, key, time, bold }) => { + let row = createElement({ type: 'tr' }); + let rowHeading = createElement({ type: 'th', innerText: title }); + + let contentText = ''; + if (time) contentText = getTimeFromTimestamp(data[key]); + else contentText = key ? data[key] : data[title.toLowerCase()]; + + let tableData = createElement({ + type: 'td', + innerText: contentText, + attributes: { + class: bold ? 'bold' : '', + }, + }); + row.appendChild(rowHeading); + row.appendChild(tableData); + tableBody.appendChild(row); + }); + + table.appendChild(tableBody); + return table; +} + +function formDataToObject(formData) { + if (!formData) return; + const result = {}; + for (const [key, value] of formData.entries()) { + result[key] = value; + } + return result; +} diff --git a/extension-requests/script.js b/extension-requests/script.js new file mode 100644 index 00000000..b4ca3c4b --- /dev/null +++ b/extension-requests/script.js @@ -0,0 +1,204 @@ +const container = document.querySelector('.container'); +const extensionRequestsContainer = document.querySelector( + '.extension-requests', +); + +const errorHeading = document.querySelector('h2#error'); +const modalParent = document.querySelector('.extension-requests-modal-parent'); +const closeModal = document.querySelectorAll('#close-modal'); + +//modal containers +const modalShowInfo = document.querySelector('.extension-requests-info'); +const modalStatusForm = document.querySelector( + '.extension-requests-status-form', +); +const modalUpdateForm = document.querySelector('.extension-requests-form'); + +const state = { + currentExtensionRequest: null, +}; + +const render = async () => { + try { + addLoader(container); + const extensionRequests = await getExtensionRequests(); + const allExtensionRequests = extensionRequests.allExtensionRequests; + allExtensionRequests.forEach((data) => { + extensionRequestsContainer.appendChild( + createExtensionRequestCard(data, extensionRequestCardHeadings), + ); + }); + } catch (error) { + errorHeading.textContent = 'Something went wrong'; + errorHeading.classList.add('error-visible'); + reload(); + } finally { + removeLoader('loader'); + } +}; +const showTaskDetails = async (taskId, approved) => { + if (!taskId) return; + try { + modalShowInfo.innerHTML = '

Task Details

'; + addLoader(modalShowInfo); + const taskDetails = await getTaskDetails(taskId); + const taskData = taskDetails.taskData; + modalShowInfo.append( + createTaskInfoModal(taskData, approved, taskInfoModelHeadings), + ); + } catch (error) { + errorHeading.textContent = 'Something went wrong'; + errorHeading.classList.add('error-visible'); + reload(); + } finally { + removeLoader('loader'); + } +}; +function createTaskInfoModal(data, approved, dataHeadings) { + if (!data) return; + + const updateStatus = createElement({ + type: 'button', + attributes: { class: 'status-form' }, + innerText: 'Update Status', + }); + const closeModal = createElement({ + type: 'button', + attributes: { id: 'close-modal' }, + innerText: 'Cancel', + }); + updateStatus.addEventListener('click', () => { + showModal('status-form'); + fillStatusForm(); + }); + closeModal.addEventListener('click', () => hideModal()); + + const main = createTable(dataHeadings, data); + + if (!approved) main.appendChild(updateStatus); + main.appendChild(closeModal); + return main; +} +function createExtensionRequestCard(data, dataHeadings) { + if (!data) return; + + const updateRequestBtn = createElement({ + type: 'button', + attributes: { class: 'update_request' }, + innerText: 'Update Request', + }); + const moreInfoBtn = createElement({ + type: 'button', + attributes: { class: 'more' }, + innerText: 'More', + }); + updateRequestBtn.addEventListener('click', () => { + showModal('update-form'); + state.currentExtensionRequest = data; + fillUpdateForm(); + }); + moreInfoBtn.addEventListener('click', () => { + showModal('info'); + showTaskDetails(data.taskId, data.status === 'APPROVED'); + state.currentExtensionRequest = data; + }); + + const main = createTable(dataHeadings, data, 'extension-request'); + + main.appendChild(moreInfoBtn); + main.appendChild(updateRequestBtn); + return main; +} +render(); + +//PATCH requests functions +async function onStatusFormSubmit(e) { + e.preventDefault(); + try { + addLoader(container); + let formData = formDataToObject(new FormData(e.target)); + await updateExtensionRequestStatus({ + id: state.currentExtensionRequest.id, + body: formData, + }); + reload(); + } catch (error) { + errorHeading.textContent = 'Something went wrong'; + errorHeading.classList.add('error-visible'); + reload(); + } finally { + removeLoader('loader'); + } +} +async function onUpdateFormSubmit(e) { + e.preventDefault(); + try { + addLoader(container); + let formData = formDataToObject(new FormData(e.target)); + formData['newEndsOn'] = new Date(formData['newEndsOn']).getTime() / 1000; + await updateExtensionRequest({ + id: state.currentExtensionRequest.id, + body: formData, + }); + reload(); + } catch (error) { + errorHeading.textContent = 'Something went wrong'; + errorHeading.classList.add('error-visible'); + reload(); + } finally { + removeLoader('loader'); + } +} + +modalUpdateForm.addEventListener('submit', onUpdateFormSubmit); +modalStatusForm.addEventListener('submit', onStatusFormSubmit); + +modalParent.addEventListener('click', hideModal); +closeModal.forEach((node) => node.addEventListener('click', () => hideModal())); + +function showModal(show = 'form') { + modalParent.classList.add('visible'); + modalParent.setAttribute('show', show); +} +function hideModal(e) { + if (!e) { + modalParent.classList.remove('visible'); + return; + } + e.stopPropagation(); + if (e.target === modalParent) { + modalParent.classList.remove('visible'); + } +} +function reload() { + setTimeout(() => window.history.go(0), 2000); +} +function fillStatusForm() { + modalStatusForm.querySelector('.extensionId').value = + state.currentExtensionRequest.id; + modalStatusForm.querySelector('.extensionTitle').value = + state.currentExtensionRequest.title; + modalStatusForm.querySelector('.extensionAssignee').value = + state.currentExtensionRequest.assignee; +} +function fillUpdateForm() { + const { newEndsOn, oldEndsOn, status, id, title, assignee, reason } = + state.currentExtensionRequest; + + modalUpdateForm.querySelector('.extensionNewEndsOn').value = new Date( + newEndsOn * 1000, + ) + .toISOString() + .replace('Z', ''); + modalUpdateForm.querySelector('.extensionOldEndsOn').value = new Date( + oldEndsOn * 1000, + ) + .toISOString() + .replace('Z', ''); + + modalUpdateForm.querySelector('.extensionStatus').value = status; + modalUpdateForm.querySelector('.extensionId').value = id; + modalUpdateForm.querySelector('.extensionTitle').value = title; + modalUpdateForm.querySelector('.extensionAssignee').value = assignee; + modalUpdateForm.querySelector('.extensionReason').value = reason; +} diff --git a/extension-requests/style.css b/extension-requests/style.css new file mode 100644 index 00000000..5f05634e --- /dev/null +++ b/extension-requests/style.css @@ -0,0 +1,261 @@ +:root { + --dark-blue: #1b1378; + --light-aqua: #d4f9f2; + --scandal: #e5fcf5; + --white: #ffffff; + --black-transparent: #000000a8; + --black: #181717; + --light-gray: #d9d9d9; + --razzmatazz: #df0057; + --gray: #808080; + --button-proceed: #008000; + --modal-color: #00000048; +} + +*, +::after, +::before { + box-sizing: border-box; +} + +button { + cursor: pointer; +} + +.bold { + font-weight: bolder; +} + +body { + font-family: 'Roboto', sans-serif; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + min-height: 100vh; + user-select: none; + max-width: 100vw; +} + +.container { + width: 100%; + text-align: center; + padding: 10px; +} + +.header { + background-color: var(--dark-blue); + text-align: center; + color: var(--white); +} + +.extension-requests-modal-parent { + display: none; +} +.extension-requests-modal-parent.visible { + z-index: 1; + position: fixed; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + min-width: 100vw; + min-height: 100vh; + background-color: var(--modal-color); + overflow-y: scroll; + padding-bottom: 1rem; +} + +.extension-requests-modal-parent > * { + display: none; + flex-direction: column; + justify-content: center; + align-items: flex-start; + background-color: var(--white); + padding: 1.25rem; + border-radius: 5px; + min-width: 250px; +} + +#close-modal { + align-self: center; +} + +/* task info */ +.extension-requests-modal-parent[show='info'] .extension-requests-info { + display: flex; +} + +.extension-requests-info { + position: absolute; + left: 30%; + right: 30%; + height: fit-content; +} + +.extension-requests-info table { + text-align: left; + margin: 10px; + color: var(--black); + border-radius: 5px; +} + +.extension-requests-info button { + background-color: var(--button-proceed); + border: none; + color: var(--white); + padding: 0.5rem 1rem; + font-size: medium; + font-weight: bold; + border-radius: 5px; + margin: 10px; +} + +/* task info ends here */ + +/* update form */ +.extension-requests-modal-parent[show='update-form'] .extension-requests-form { + display: flex; +} + +.extension-requests-form { + position: absolute; + left: 30%; + right: 30%; + height: min-content; + overflow-y: scroll; + top: 10%; + bottom: 10%; +} + +.extension-requests-form input, +.extension-requests-form select { + margin: 5px 0px 15px 0px; + padding: 5px; + width: 100%; + border: none; + border-bottom: 1px solid var(--gray); +} +.extension-requests-form input:focus { + outline: none; + border-bottom: 1px solid var(--gray); +} +.extension-requests-form button { + background-color: var(--button-proceed); + border: none; + color: var(--white); + padding: 0.5rem 1rem; + font-size: medium; + font-weight: bold; + border-radius: 5px; + margin: 10px; +} + +/* update form ends here */ + +/* status form */ +.extension-requests-modal-parent[show='status-form'] + .extension-requests-status-form { + display: flex; +} + +.extension-requests-status-form input, +.extension-requests-status-form select { + margin: 5px 0px 15px 0px; + padding: 5px; + width: 100%; + border: none; + border-bottom: 1px solid var(--gray); +} +.extension-requests-status-form input:focus { + outline: none; + border-bottom: 1px solid var(--gray); +} +.extension-requests-status-form button { + background-color: var(--button-proceed); + border: none; + color: var(--white); + padding: 0.5rem 1rem; + font-size: medium; + font-weight: bold; + border-radius: 5px; + margin: 10px; +} + +/* status form ends here */ + +.extension-requests { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + flex-wrap: wrap; +} + +.extension-request { + text-align: left; + border: 2px solid var(--black-transparent); + padding: 1.25rem; + margin: 10px; + color: var(--black); + border-radius: 5px; +} + +.extension-request tr th { + width: 50%; +} + +.extension-request .more, +.extension-request .update_request { + background-color: var(--button-proceed); + border: none; + color: var(--white); + width: 40%; + min-width: max-content; + padding: 0.5rem 1rem; + font-size: medium; + font-weight: bold; + border-radius: 5px; + margin: 10px 10px 0px 0px; +} + +/* Loader Container */ +.loader-text { + text-align: center; + font-size: 2rem; +} +.loader { + margin: auto auto; +} + +.loader p { + font-weight: 800; + font-size: 3em; +} + +/* Error Heading */ +.error { + display: none; +} +.error-visible { + display: block; +} + +@media screen and (max-width: 800px) { + .extension-request { + margin: 0; + padding: 0.8rem; + } + + .extension-request > tr > th { + max-width: fit-content; + } + + .extension-requests-info { + left: 10%; + right: 10%; + } + .extension-requests-form { + left: 10%; + right: 10%; + } +} diff --git a/index.html b/index.html index f0a947b5..4c308f0c 100644 --- a/index.html +++ b/index.html @@ -35,6 +35,13 @@ > User Management + + Extension Requests + Online Members diff --git a/script.js b/script.js index fc3c9f6c..b5316022 100644 --- a/script.js +++ b/script.js @@ -1,15 +1,27 @@ const userManagementLink = document.getElementById(USER_MANAGEMENT_LINK); -export async function showUserManagementButton() { +const extensionRequestsLink = document.getElementById(EXTENSION_REQUESTS_LINK); + +export async function showSuperUserOptions(...privateBtns) { try { const isSuperUser = await checkUserIsSuperUser(); if (isSuperUser) { - userManagementLink.classList.remove('element-display-remove'); + privateBtns.forEach((btn) => + btn.classList.remove('element-display-remove'), + ); } } catch (err) { console.log(err); } } +/* + * To show the super user options only to the super user, give all those + * buttons or node the class "element-display-remove" so by default they are hidden. + * Then get the node from the DOM into a variable and pass that variable in the + * function below. + */ +showSuperUserOptions(userManagementLink, extensionRequestsLink); + const createGoalButton = document.getElementById('create-goal'); const params = new URLSearchParams(window.location.search); if (params.get('dev') === 'true') { diff --git a/style.css b/style.css index 1651573e..36181d15 100644 --- a/style.css +++ b/style.css @@ -36,8 +36,9 @@ body { height: 80vh; display: flex; justify-content: center; - align-items: center; + align-content: center; gap: 20px; + flex-wrap: wrap; } button { @@ -45,6 +46,24 @@ button { cursor: pointer; } +.create-task-btn { + color: black; + font-weight: 500; + font-size: larger; + background-color: white; + border: 2px solid black; + border-radius: 5px; + padding: 10px 50px; + cursor: pointer; + transition: all 0.5s ease; + outline: none; +} +.create-task-btn:hover { + color: white; + background-color: #1d1283; + border-color: transparent; +} + /* profile section */ .profileSection { height: 80vh; @@ -53,8 +72,79 @@ button { align-items: center; } +.profile-task-btn { + color: black; + font-weight: 500; + font-size: larger; + background-color: white; + border: 2px solid black; + border-radius: 5px; + padding: 10px 50px; + cursor: pointer; + transition: all 0.5s ease; +} +.profile-task-btn:hover { + color: white; + background-color: #1d1283; + border-color: transparent; +} +#user-management-link { + color: black; + font-weight: 500; + font-size: larger; + background-color: var(--white-color); + border: 2px solid var(--black-color); + border-radius: 5px; + padding: 10px 50px; + cursor: pointer; + transition: all 0.5s ease; + width: 270px; + text-decoration: none; +} +#user-management-link:hover { + color: var(--white-color); + background-color: var(--blue-color); + border-color: transparent; +} + +/* Extension Requests */ +#extension-requests-link { + color: black; + font-weight: 500; + font-size: larger; + background-color: var(--white-color); + border: 2px solid var(--black-color); + border-radius: 5px; + padding: 10px 50px; + cursor: pointer; + transition: all 0.5s ease; + width: max-content; + text-decoration: none; +} +#extension-requests-link:hover { + color: var(--white-color); + background-color: var(--blue-color); + border-color: transparent; +} + /* Online-members */ +.online-members-link { + color: black; + font-weight: 500; + font-size: larger; + border: 2px solid black; + border-radius: 5px; + padding: 10px 50px; + transition: all 0.5s ease; +} + +.online-members-link:hover { + color: white; + background-color: #1d1283; + border-color: transparent; +} +/* Online-members */ .info-repo { font-weight: 100; padding: auto; diff --git a/utils.js b/utils.js index 4ea4c951..b7bf4176 100644 --- a/utils.js +++ b/utils.js @@ -18,3 +18,31 @@ async function checkUserIsSuperUser() { const self_user = await getSelfUser(); return self_user?.roles['super_user']; } + +function createElement({ type, attributes = {}, innerText }) { + const element = document.createElement(type); + Object.keys(attributes).forEach((item) => { + element.setAttribute(item, attributes[item]); + }); + element.textContent = innerText; + return element; +} + +function addLoader(container) { + if (!container) return; + const loader = createElement({ + type: 'div', + attributes: { class: 'loader' }, + }); + const loadertext = createElement({ + type: 'p', + attributes: { class: 'loader-text' }, + innerText: 'Loading...', + }); + loader.appendChild(loadertext); + container.appendChild(loader); +} + +function removeLoader(classname) { + document.querySelector(`.${classname}`).remove(); +}