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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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();
+}