diff --git a/__tests__/taskRequests/taskRequest.test.js b/__tests__/taskRequests/taskRequest.test.js
new file mode 100644
index 00000000..0a083e2b
--- /dev/null
+++ b/__tests__/taskRequests/taskRequest.test.js
@@ -0,0 +1,289 @@
+const puppeteer = require('puppeteer');
+
+const { fetchedTaskRequests } = require('../../mock-data/taskRequests');
+
+const SITE_URL = 'http://localhost:8000';
+// helper/loadEnv.js file causes API_BASE_URL to be stagin-api on local env url in taskRequest/index.html
+const API_BASE_URL = 'https://staging-api.realdevsquad.com';
+
+describe('Task Requests', () => {
+ let browser;
+ let page;
+
+ jest.setTimeout(60000);
+
+ beforeEach(async () => {
+ browser = await puppeteer.launch({
+ headless: 'new',
+ ignoreHTTPSErrors: true,
+ args: ['--incognito', '--disable-web-security'],
+ devtools: false,
+ });
+ });
+ beforeEach(async () => {
+ page = await browser.newPage();
+
+ await page.setRequestInterception(true);
+
+ page.on('request', (request) => {
+ if (
+ request.url() === `${API_BASE_URL}/taskRequests` ||
+ request.url() === `${API_BASE_URL}/taskRequests?dev=true` ||
+ request.url() ===
+ `${API_BASE_URL}/taskRequests?size=20&q=status%3Apending+sort%3Acreated-asc&dev=true`
+ ) {
+ request.respond({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ data: fetchedTaskRequests }),
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
+ },
+ });
+ } else if (
+ request.url() ===
+ `${API_BASE_URL}/taskRequests?size=20&q=status%3Aapproved++sort%3Acreated-asc&dev=true`
+ ) {
+ const list = [];
+ for (let i = 0; i < 20; i++) {
+ list.push(fetchedTaskRequests[0]);
+ }
+ request.respond({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({
+ data: list,
+ next: '/taskRequests?size=20&q=status%3Aapproved++sort%3Acreated-asc&dev=true',
+ }),
+ });
+ } else {
+ request.continue();
+ }
+ });
+ await page.goto(`${SITE_URL}/taskRequests`);
+ await page.waitForNetworkIdle();
+ });
+
+ afterEach(async () => {
+ await page.close();
+ });
+
+ afterAll(async () => {
+ await browser.close();
+ });
+ describe('When the user is super user', () => {
+ it('should display the task requests card', async () => {
+ const url = await page.evaluate(() => API_BASE_URL);
+ const taskCards = await page.$$('.taskRequest__card');
+ const title = await taskCards[0].evaluate(
+ (el) => el.children[0].textContent,
+ );
+ const purpose = await taskCards[0].evaluate(
+ (el) => el.children[1].textContent,
+ );
+
+ expect(taskCards).toHaveLength(1);
+ expect(title).toMatch(/test title/i);
+ expect(purpose).toMatch(/test purpose/i);
+ });
+ describe('Filter Modal', () => {
+ it('should be hidden initially', async () => {
+ const modal = await page.$('.filter-modal');
+ expect(
+ await modal.evaluate((el) => el.classList.contains('hidden')),
+ ).toBe(true);
+ });
+
+ it('should be displayed after clicking the filter button and hidden on outside click', async () => {
+ const modal = await page.$('.filter-modal');
+ const filterHead = await page.$('.filter-head');
+ const filterContainer = await page.$('.filters-container');
+ expect(filterHead).toBeTruthy();
+ expect(filterContainer).toBeTruthy();
+ await page.click('#filter-button');
+ expect(modal).not.toBeNull();
+ expect(
+ await modal.evaluate((el) => el.classList.contains('hidden')),
+ ).toBe(false);
+ await page.mouse.click(200, 200);
+ expect(
+ await modal.evaluate((el) => el.classList.contains('hidden')),
+ ).toBe(true);
+ });
+
+ it('checks if PENDING is checked by default', async () => {
+ const filterButton = await page.$('#filter-button');
+ await filterButton.click();
+ await page.waitForSelector('.filter-modal');
+ const activeFilter = await page.$('input[value="PENDING"]');
+ const currentState = await activeFilter.getProperty('checked');
+ const isChecked = await currentState.jsonValue();
+ expect(isChecked).toBe(true);
+ });
+
+ it('Selecting filters and clicking on apply should filter task request list', async () => {
+ let cardsList = await page.$$('.taskRequest__card');
+ expect(cardsList).not.toBeNull();
+ const initialLength = cardsList.length;
+ await page.click('#filter-button');
+ await page.click('input[value="PENDING"]');
+ await page.click('input[value="APPROVED"]');
+ await page.click('#apply-filter-button');
+ await page.waitForNetworkIdle();
+ cardsList = await page.$$('.taskRequest__card');
+ expect(cardsList).not.toBeNull();
+ expect(cardsList.length).toBeGreaterThanOrEqual(0);
+ expect(cardsList.length).not.toBe(initialLength);
+ });
+
+ it('clears the filter when the Clear button is clicked', async () => {
+ const filterButton = await page.$('#filter-button');
+ await filterButton.click();
+ await page.waitForSelector('.filter-modal');
+ const activeFilter = await page.$('input[value="APPROVED"]');
+ await activeFilter.click();
+ const clearButton = await page.$('.filter-modal #clear-button');
+ await clearButton.click();
+ await page.waitForSelector('.filter-modal', { hidden: true });
+ const currentState = await activeFilter.getProperty('checked');
+ const isChecked = await currentState.jsonValue();
+ expect(isChecked).toBe(false);
+ });
+ });
+
+ describe('Sort Modal', () => {
+ it('should be hidden initially', async () => {
+ const sortModal = await page.$('.sort-modal');
+ const assigneButton = await page.$('#REQUESTORS_COUNT_ASC');
+ expect(
+ await sortModal.evaluate((el) => el.classList.contains('hidden')),
+ ).toBe(true);
+ expect(assigneButton).toBeTruthy();
+ });
+
+ it('should toggle visibility sort modal by clicking the sort button and selecting an option', async () => {
+ const sortModal = await page.$('.sort-modal');
+ const assigneButton = await page.$('#REQUESTORS_COUNT_ASC');
+ const sortHead = await page.$('.sort-head');
+ const sortContainer = await page.$('.sorts-container');
+
+ expect(sortHead).toBeTruthy();
+ expect(sortContainer).toBeTruthy();
+
+ await page.click('.sort-button');
+ await page.click('#REQUESTORS_COUNT_ASC');
+ expect(
+ await assigneButton.evaluate((el) =>
+ el.classList.contains('selected'),
+ ),
+ ).toBe(true);
+ expect(
+ await sortModal.evaluate((el) => el.classList.contains('hidden')),
+ ).toBe(true);
+ await page.click('.sort-button');
+ await page.click('#REQUESTORS_COUNT_ASC');
+ expect(
+ await assigneButton.evaluate((el) =>
+ el.classList.contains('selected'),
+ ),
+ ).toBe(false);
+ expect(
+ await sortModal.evaluate((el) => el.classList.contains('hidden')),
+ ).toBe(true);
+ });
+ });
+
+ it('Checks that new items are loaded when scrolled to the bottom', async () => {
+ await page.click('#filter-button');
+ await page.click('input[value="PENDING"]');
+ await page.click('input[value="APPROVED"]');
+ await page.click('#apply-filter-button');
+ await page.waitForNetworkIdle();
+ let taskRequestList = await page.$$('.taskRequest__card');
+ expect(taskRequestList.length).toBe(20);
+ await page.evaluate(() => {
+ const element = document.querySelector('.virtual');
+ if (element) {
+ element.scrollIntoView({ behavior: 'auto' });
+ }
+ });
+ await page.waitForNetworkIdle();
+ taskRequestList = await page.$$('.taskRequest__card');
+ expect(taskRequestList.length).toBe(40);
+ });
+ });
+});
+
+describe('createCustomElement', () => {
+ let browser;
+ let page;
+
+ beforeAll(async () => {
+ browser = await puppeteer.launch({
+ headless: 'new',
+ });
+
+ page = await browser.newPage();
+
+ await page.goto(`${SITE_URL}/taskRequests`);
+ await page.waitForNetworkIdle();
+ });
+
+ afterAll(async () => {
+ await browser.close();
+ });
+
+ describe('tagName', () => {
+ it('should create tag with provided tagName', async () => {
+ const tag = await page.evaluate(
+ () => createCustomElement({ tagName: 'p' }).tagName,
+ );
+ expect(tag).toMatch(/p/i);
+ });
+
+ it('should not add tagName attribute', async () => {
+ const tagNameAttr = await page.evaluate(() =>
+ createCustomElement({ tagName: 'p' }).getAttribute('tagName'),
+ );
+
+ expect(tagNameAttr).toBeNull();
+ });
+ });
+
+ describe('className', () => {
+ it('should add the class when class key is provided using string', async () => {
+ const classes = await page.evaluate(() => [
+ ...createCustomElement({ tagName: 'p', class: 'test-class' }).classList,
+ ]);
+
+ expect(classes).toHaveLength(1);
+ expect(classes).toContain('test-class');
+ });
+
+ it('should add multiple classes when class key has array as value', async () => {
+ const classes = await page.evaluate(() => [
+ ...createCustomElement({
+ tagName: 'p',
+ class: ['test-class-1', 'test-class-2'],
+ }).classList,
+ ]);
+
+ expect(classes).toHaveLength(2);
+ expect(classes).toStrictEqual(['test-class-1', 'test-class-2']);
+ });
+ });
+
+ describe('textContent', () => {
+ it('should add textContent key when specified', async () => {
+ const textContent = await page.evaluate(
+ () =>
+ createCustomElement({ tagName: 'p', textContent: 'test content' })
+ .textContent,
+ );
+
+ expect(textContent).toBe('test content');
+ });
+ });
+});
diff --git a/__tests__/taskRequests/taskRequestDetails.test.js b/__tests__/taskRequests/taskRequestDetails.test.js
new file mode 100644
index 00000000..b29ce8c0
--- /dev/null
+++ b/__tests__/taskRequests/taskRequestDetails.test.js
@@ -0,0 +1,179 @@
+const puppeteer = require('puppeteer');
+const {
+ urlMappings,
+ defaultMockResponseHeaders,
+} = require('../../mock-data/taskRequests');
+
+describe('Task request details page', () => {
+ let browser;
+ let page;
+ jest.setTimeout(60000);
+
+ beforeAll(async () => {
+ browser = await puppeteer.launch({
+ headless: 'new',
+ ignoreHTTPSErrors: true,
+ args: ['--incognito', '--disable-web-security'],
+ devtools: false,
+ });
+ page = await browser.newPage();
+ await page.setRequestInterception(true);
+ page.on('request', (interceptedRequest) => {
+ const url = interceptedRequest.url();
+ if (urlMappings.hasOwnProperty(url)) {
+ interceptedRequest.respond({
+ ...defaultMockResponseHeaders,
+ body: JSON.stringify(urlMappings[url]),
+ });
+ } else {
+ interceptedRequest.continue();
+ }
+ });
+ await page.goto(
+ 'http://localhost:8000/taskRequests/details/?id=dM5wwD9QsiTzi7eG7Oq5',
+ );
+ });
+
+ afterAll(async () => {
+ await browser.close();
+ });
+
+ it('Checks the Modal working as expected', async () => {
+ await page.waitForNetworkIdle();
+ await page.click('.info__more');
+ await page.waitForSelector('#requestor_details_modal_content', {
+ visible: true,
+ });
+ const modalHeading = await page.$eval(
+ '[data-modal-header="requestor-details-header"]',
+ (element) => element.textContent,
+ );
+ expect(modalHeading).toBe('Requestor Details');
+
+ const proposedStartDateHeading = await page.$eval(
+ '[data-modal-start-date-text="proposed-start-date-text"]',
+ (element) => element.textContent,
+ );
+ expect(proposedStartDateHeading).toBe('Proposed Start Date:');
+
+ const proposedStartDateValue = await page.$eval(
+ '[data-modal-start-date-value="proposed-start-date-value"]',
+ (element) => element.textContent,
+ );
+ expect(proposedStartDateValue).toBe('30-10-2023');
+
+ const proposedEndDateHeading = await page.$eval(
+ '[data-modal-end-date-text="proposed-end-date-text"]',
+ (element) => element.textContent,
+ );
+ expect(proposedEndDateHeading).toBe('Proposed Deadline:');
+
+ const proposedEndDateValue = await page.$eval(
+ '[data-modal-end-date-value="proposed-end-date-value"]',
+ (element) => element.textContent,
+ );
+ expect(proposedEndDateValue).toBe('5-11-2023');
+
+ const descriptionTextHeading = await page.$eval(
+ '[data-modal-description-text="proposed-description-text"]',
+ (element) => element.textContent,
+ );
+ expect(descriptionTextHeading).toBe('Description:');
+
+ const descriptionTextValue = await page.$eval(
+ '[data-modal-description-value="proposed-description-value"]',
+ (element) => element.textContent,
+ );
+ expect(descriptionTextValue).toBe(
+ 'code change 3 days , testing - 2 days. total - 5 days',
+ );
+ });
+
+ it('Should contain Approve and Reject buttons', async function () {
+ const approveButton = await page.$('.requestors__conatainer__list__button');
+ const rejectButton = await page.$('.request-details__reject__button');
+ expect(approveButton).toBeTruthy();
+ expect(rejectButton).toBeTruthy();
+ });
+});
+
+describe('Task request details page with status creation', () => {
+ let browser;
+ let page;
+ jest.setTimeout(60000);
+
+ beforeAll(async () => {
+ browser = await puppeteer.launch({
+ headless: 'new',
+ ignoreHTTPSErrors: true,
+ args: ['--incognito', '--disable-web-security'],
+ devtools: false,
+ });
+ page = await browser.newPage();
+ await page.setRequestInterception(true);
+ page.on('request', (interceptedRequest) => {
+ const url = interceptedRequest.url();
+ if (urlMappings.hasOwnProperty(url)) {
+ interceptedRequest.respond({
+ ...defaultMockResponseHeaders,
+ body: JSON.stringify(urlMappings[url]),
+ });
+ } else {
+ interceptedRequest.continue();
+ }
+ });
+ await page.goto(
+ 'http://localhost:8000/taskRequests/details/?id=uC0IUpkFMx393XjnKx4w',
+ );
+ });
+
+ afterAll(async () => {
+ await browser.close();
+ });
+
+ it('Should render github issue', async () => {
+ await page.waitForNetworkIdle();
+
+ const issue = await page.$('#task-details');
+ const testId = await issue.evaluate((el) => el.innerHTML);
+
+ expect(testId).toContain(
+ 'When super_user try to update skills of new users the data of',
+ );
+ });
+ it('Should render title of the issue', async () => {
+ await page.waitForNetworkIdle();
+ const issueTitle = await page.$('#issue_title');
+ const title = await issueTitle.evaluate((el) => el.innerHTML);
+
+ expect(title).toBe(
+ 'Fix: user data is not showing up in memberSkillsUpdateModal',
+ );
+ });
+ it('Should render author and time of the issue', async () => {
+ await page.waitForNetworkIdle();
+ const issueTimeAndAuthor = await page.$('#issue_time_author');
+ const timeAndAuthor = await issueTimeAndAuthor.evaluate(
+ (el) => el.innerHTML,
+ );
+
+ expect(timeAndAuthor).toContain('Wed Sep 06 2023');
+ expect(timeAndAuthor).toContain('anishpawaskar');
+ });
+ it('Should render assignee of the issue', async () => {
+ await page.waitForNetworkIdle();
+ const issueAssignee = await page.$('#issue_assignee');
+ const assignee = await issueAssignee.evaluate((el) => el.innerHTML);
+
+ expect(assignee).toContain('anishpawaskar');
+ });
+ it('Should render link of the issue', async () => {
+ await page.waitForNetworkIdle();
+ const issueLink = await page.$('#issue_link');
+ const link = await issueLink.evaluate((el) => el.innerHTML);
+
+ expect(link).toContain(
+ 'https://github.com/Real-Dev-Squad/members-site/issues/92',
+ );
+ });
+});
diff --git a/taskRequests/assets/RDSLogo.png b/taskRequests/assets/RDSLogo.png
new file mode 100644
index 00000000..7f10e48f
Binary files /dev/null and b/taskRequests/assets/RDSLogo.png differ
diff --git a/taskRequests/assets/funnel.svg b/taskRequests/assets/funnel.svg
new file mode 100644
index 00000000..4af43b94
--- /dev/null
+++ b/taskRequests/assets/funnel.svg
@@ -0,0 +1,3 @@
+
diff --git a/taskRequests/assets/sort-down.svg b/taskRequests/assets/sort-down.svg
new file mode 100644
index 00000000..9f51c3ba
--- /dev/null
+++ b/taskRequests/assets/sort-down.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/taskRequests/assets/sort-menu.svg b/taskRequests/assets/sort-menu.svg
new file mode 100644
index 00000000..8fa429b1
--- /dev/null
+++ b/taskRequests/assets/sort-menu.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/taskRequests/assets/sort-up.svg b/taskRequests/assets/sort-up.svg
new file mode 100644
index 00000000..e991c745
--- /dev/null
+++ b/taskRequests/assets/sort-up.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/taskRequests/constants.js b/taskRequests/constants.js
new file mode 100644
index 00000000..ca84f255
--- /dev/null
+++ b/taskRequests/constants.js
@@ -0,0 +1,50 @@
+const taskRequestStatus = {
+ WAITING: 'WAITING',
+ APPROVED: 'APPROVED',
+};
+const DEV_FEATURE_FLAG = 'dev';
+const DEFAULT_PAGE_SIZE = 20;
+const Status = {
+ APPROVED: 'approved',
+ PENDING: 'pending',
+ DENIED: 'denied',
+};
+
+const Order = {
+ REQUESTORS_COUNT_ASC: { requestors: 'asc' },
+ REQUESTORS_COUNT_DESC: { requestors: 'desc' },
+ CREATED_TIME_DESC: { created: 'desc' },
+ CREATED_TIME_ASC: { created: 'asc' },
+};
+
+const FILTER_MODAL = 'filter-modal';
+const FILTER_BUTTON = 'filter-button';
+const APPLY_FILTER_BUTTON = 'apply-filter-button';
+const SEARCH_ELEMENT = 'assignee-search';
+const SORT_BUTTON = '.sort-button';
+const CLEAR_BUTTON = 'clear-button';
+const FILTER_CONTAINER = '.sort-filters';
+const SORT_MODAL = 'sort-modal';
+const ASSIGNEE_COUNT = 'REQUESTORS_COUNT_ASC';
+const ASSIGNEE_DESC = 'REQUESTORS_COUNT_DESC';
+const CREATED_TIME = 'CREATED_TIME_ASC';
+const CREATED_TIME_DESC = 'CREATED_TIME_DESC';
+const BACKDROP = '.backdrop';
+const LAST_ELEMENT_CONTAINER = '.virtual';
+
+const MessageStatus = {
+ SUCCESS: 'SUCCESS',
+ ERROR: 'ERROR',
+};
+
+const TaskRequestAction = {
+ APPROVE: 'approve',
+ REJECT: 'reject',
+};
+const ErrorMessages = {
+ UNAUTHENTICATED:
+ 'You are unauthenticated to view this section, please login!',
+ UNAUTHORIZED: 'You are unauthrozed to view this section',
+ NOT_FOUND: 'Task Requests not found',
+ SERVER_ERROR: 'Unexpected error occurred',
+};
diff --git a/taskRequests/details/index.html b/taskRequests/details/index.html
new file mode 100644
index 00000000..24f3c236
--- /dev/null
+++ b/taskRequests/details/index.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Task Requests | Real Dev Squad
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/taskRequests/details/script.js b/taskRequests/details/script.js
new file mode 100644
index 00000000..88879116
--- /dev/null
+++ b/taskRequests/details/script.js
@@ -0,0 +1,583 @@
+const API_BASE_URL = window.API_BASE_URL;
+
+let taskRequest;
+
+const taskRequestSkeleton = document.querySelector('.taskRequest__skeleton');
+const container = document.querySelector('.container');
+const taskSkeleton = document.querySelector('.task__skeleton');
+const requestorSkeleton = document.querySelector(
+ '.requestors__container__list__skeleton',
+);
+
+const taskRequestContainer = document.getElementById('task-request-details');
+const taskContainer = document.getElementById('task-details');
+const toast = document.getElementById('toast_task_details');
+const rejectButton = document.getElementById('reject-button');
+const requestorsContainer = document.getElementById('requestors-details');
+const taskRequestId = new URLSearchParams(window.location.search).get('id');
+history.pushState({}, '', window.location.href);
+const errorMessage =
+ 'The requested operation could not be completed. Please try again later.';
+let taskId;
+
+function renderTaskRequestDetails(taskRequest) {
+ taskRequestContainer.append(
+ createCustomElement({
+ tagName: 'h1',
+ textContent: `Task Request `,
+ class: 'taskRequest__title',
+ child: [
+ createCustomElement({
+ tagName: 'span',
+ class: 'taskRequest__title__subtitle',
+ textContent: `#${taskRequest?.id}`,
+ }),
+ ],
+ }),
+ createCustomElement({
+ tagName: 'p',
+ textContent: 'Status: ',
+ class: 'taskRequest__status',
+ child: [
+ createCustomElement({
+ tagName: 'span',
+ textContent: taskRequest?.status,
+ id: 'taskRequest__status_text',
+ class: [
+ 'taskRequest__status__chip',
+ `taskRequest__status__chip--${taskRequest?.status?.toLowerCase()}`,
+ ],
+ }),
+ ],
+ }),
+ createCustomElement({
+ tagName: 'p',
+ textContent: 'Request Type: ',
+ class: 'taskRequest__status',
+ child: [
+ createCustomElement({
+ tagName: 'span',
+ textContent: taskRequest?.requestType || 'ASSIGNMENT',
+ class: [
+ 'taskRequest__status__chip',
+ `taskRequest__status__chip--tag`,
+ ],
+ }),
+ ],
+ }),
+ );
+}
+
+function updateStatus(status) {
+ const statusText = document.getElementById('taskRequest__status_text');
+ statusText.classList = [];
+ statusText.classList.add('taskRequest__status__chip');
+ statusText.classList.add(
+ `taskRequest__status__chip--${status?.toLowerCase()}`,
+ );
+ statusText.textContent = status;
+}
+
+async function renderTaskDetails(taskRequest) {
+ const { taskId, taskTitle } = taskRequest;
+ try {
+ requestorsContainer.classList.add('requester-border');
+ const res = await fetch(`${API_BASE_URL}/tasks/${taskId}/details`);
+ taskSkeleton.classList.add('hidden');
+ const data = await res.json();
+ let taskReqAssigneeName = await getAssigneeName();
+
+ const { taskData } = data ?? {};
+
+ taskContainer.append(
+ createCustomElement({
+ tagName: 'h2',
+ class: 'task__title',
+ textContent: taskData?.title || taskTitle || 'N/A',
+ }),
+ createCustomElement({
+ tagName: 'p',
+ class: 'task_type',
+ textContent: 'Type: ',
+ child: [
+ taskData?.type
+ ? createCustomElement({
+ tagName: 'span',
+ class: [
+ 'task__type__chip',
+ `task__type__chip--${taskData?.type}`,
+ ],
+ textContent: taskData?.type,
+ })
+ : '',
+ taskData?.isNoteworthy
+ ? createCustomElement({
+ tagName: 'span',
+ class: ['task__type__chip', `task__type__chip--noteworthy`],
+ textContent: 'Note worthy',
+ })
+ : '',
+ ],
+ }),
+ createCustomElement({
+ tagName: 'p',
+ class: 'task__createdBy',
+ textContent: `Created By: `,
+ child: [
+ createCustomElement({
+ tagName: 'a',
+ href: `https://members.realdevsquad.com/${taskData?.createdBy}`,
+ textContent: taskData?.createdBy || 'N/A',
+ }),
+ ],
+ }),
+ createCustomElement({
+ tagName: 'p',
+ class: 'task__createdBy',
+ textContent: `Purpose : ${taskData?.purpose ?? 'N/A'}`,
+ }),
+ );
+ renderAssignedTo(taskReqAssigneeName);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+function getAvatar(user) {
+ if (user?.user?.picture?.url) {
+ return createCustomElement({
+ tagName: 'img',
+ src: user?.user?.picture?.url,
+ alt: user?.user?.first_name,
+ title: user?.user?.first_name,
+ className: 'circular-image',
+ });
+ }
+ return createCustomElement({
+ tagName: 'span',
+ title: user?.user?.first_name,
+ textContent: user?.user?.first_name[0],
+ });
+}
+
+async function updateTaskRequest(action, userId) {
+ const removeSpinner = addSpinner(container);
+ container.classList.add('container-disabled');
+ try {
+ const queryParams = new URLSearchParams({ action: action });
+ const res = await fetch(`${API_BASE_URL}/taskRequests?${queryParams}`, {
+ credentials: 'include',
+ method: 'PATCH',
+ body: JSON.stringify({
+ taskRequestId,
+ userId,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (res.ok) {
+ showToast('Task updated Successfully', 'success');
+ taskRequest = await fetchTaskRequest();
+ requestorsContainer.innerHTML = '';
+ updateStatus(taskRequest.status);
+ renderRequestors(taskRequest);
+ renderRejectButton(taskRequest);
+ return res;
+ } else {
+ showToast(errorMessage, 'failure');
+ }
+ } catch (e) {
+ showToast(errorMessage, 'failure');
+ console.error(e);
+ } finally {
+ removeSpinner();
+ container.classList.remove('container-disabled');
+ }
+}
+
+function getActionButton(requestor) {
+ if (taskRequest?.status === taskRequestStatus.APPROVED) {
+ if (taskRequest.approvedTo === requestor?.user?.id) {
+ return createCustomElement({
+ tagName: 'p',
+ textContent: 'Approved',
+ class: ['requestors__container__list__approved'],
+ });
+ } else {
+ return '';
+ }
+ }
+ return createCustomElement({
+ tagName: 'button',
+ textContent: 'Approve',
+ class: 'requestors__conatainer__list__button',
+ eventListeners: [
+ {
+ event: 'click',
+ func: () =>
+ updateTaskRequest(TaskRequestAction.APPROVE, requestor.user?.id),
+ },
+ ],
+ });
+}
+
+async function renderRequestors(taskRequest) {
+ const requestors = taskRequest?.users;
+ requestorSkeleton.classList.remove('hidden');
+ const data = await Promise.all(
+ requestors.map((requestor) => {
+ return fetch(`${API_BASE_URL}/users/userId/${requestor.userId}`).then(
+ (res) => res.json(),
+ );
+ }),
+ );
+
+ requestorSkeleton.classList.add('hidden');
+
+ data.forEach((requestor, index) => {
+ const userDetailsDiv = createCustomElement({
+ tagName: 'li',
+ child: [
+ createCustomElement({
+ tagName: 'div',
+ class: 'requestors__container__list__userDetails',
+ child: [
+ createCustomElement({
+ tagName: 'div',
+ class: 'requestors__container__list__userDetails__avatar',
+ child: [getAvatar(requestor)],
+ }),
+ createCustomElement({
+ tagName: 'div',
+ class: 'requestors__container__list__userDetails__info',
+ child: [
+ createCustomElement({
+ tagName: 'p',
+ class: 'info__name',
+ textContent: requestor.user?.first_name,
+ }),
+ createCustomElement({
+ tagName: 'a',
+ textContent: 'details>',
+ class: 'info__more',
+ eventListeners: [
+ {
+ event: 'click',
+ func: () => populateModalContent(index),
+ },
+ ],
+ }),
+ ],
+ }),
+ ],
+ }),
+ createCustomElement({
+ tagName: 'div',
+ child: [
+ taskRequest.status !== 'DENIED' ? getActionButton(requestor) : '',
+ ],
+ }),
+ ],
+ });
+ const avatarDiv = userDetailsDiv.querySelector(
+ '.requestors__container__list__userDetails__avatar',
+ );
+ requestorsContainer.append(userDetailsDiv);
+ });
+}
+
+async function fetchTaskRequest() {
+ const res = await fetch(`${API_BASE_URL}/taskRequests/${taskRequestId}`, {
+ credentials: 'include',
+ });
+
+ const { data } = await res.json();
+ const approvedTo = data.users
+ .filter((user) => user.status === 'APPROVED')
+ ?.map((user) => user.userId)?.[0];
+ data.approvedTo = approvedTo;
+ return data;
+}
+
+const renderGithubIssue = async () => {
+ converter = new showdown.Converter({
+ tables: true,
+ simplifiedAutoLink: true,
+ tasklists: true,
+ simplifiedAutoLink: true,
+ ghCodeBlocks: true,
+ openLinksInNewWindow: true,
+ });
+ let res = await fetch(taskRequest?.externalIssueUrl);
+ res = await res.json();
+ taskSkeleton.classList.add('hidden');
+ taskContainer.classList.add('task__issue__container');
+ taskContainer.append(
+ createCustomElement({
+ tagName: 'h2',
+ innerHTML: res?.title,
+ id: 'issue_title',
+ }),
+ );
+ taskContainer.appendChild(
+ createCustomElement({
+ tagName: 'p',
+ id: 'issue_time_author',
+ child: [
+ createCustomElement({
+ tagName: 'span',
+ textContent:
+ 'Opened on ' + new Date(res?.created_at).toDateString() + ' by ',
+ }),
+ createCustomElement({
+ tagName: 'a',
+ href: res?.user?.html_url,
+ textContent: res?.user?.login,
+ }),
+ ],
+ }),
+ );
+ html = converter.makeHtml(res?.body);
+ taskContainer.appendChild(
+ createCustomElement({
+ tagName: 'div',
+ innerHTML: html,
+ }),
+ );
+
+ if (res?.assignee) {
+ taskContainer.appendChild(
+ createCustomElement({
+ tagName: 'p',
+ id: 'issue_assignee',
+ child: [
+ createCustomElement({
+ tagName: 'span',
+ child: [
+ createCustomElement({
+ tagName: 'span',
+ textContent: 'Assigned to: ',
+ }),
+ createCustomElement({
+ tagName: 'a',
+ class: 'card__link',
+ textContent: res?.assignee?.login,
+ href: res?.assignee?.html_url,
+ }),
+ ],
+ }),
+ ],
+ }),
+ );
+ }
+ taskContainer.appendChild(
+ createCustomElement({
+ tagName: 'p',
+ id: 'issue_link',
+ class: 'card__link_issue',
+ child: [
+ createCustomElement({
+ tagName: 'span',
+ textContent: 'Issue link: ',
+ }),
+ createCustomElement({
+ tagName: 'a',
+ class: 'card__link',
+ textContent: res?.html_url,
+ href: res?.html_url || '#',
+ }),
+ ],
+ }),
+ );
+ taskContainer.appendChild(
+ createCustomElement({
+ tagName: 'div',
+ child: res?.labels.map((label) =>
+ createCustomElement({
+ tagName: 'button',
+ textContent: label?.name,
+ class: 'card__tag',
+ }),
+ ),
+ }),
+ );
+};
+const renderRejectButton = (taskRequest) => {
+ if (taskRequest?.status !== 'PENDING') {
+ rejectButton.disabled = true;
+ }
+
+ rejectButton.addEventListener('click', async () => {
+ const res = await updateTaskRequest(TaskRequestAction.REJECT);
+ if (res?.ok) {
+ rejectButton.disabled = true;
+ }
+ });
+};
+const renderTaskRequest = async () => {
+ taskRequestSkeleton.classList.remove('hidden');
+ taskContainer.classList.remove('hidden');
+ try {
+ taskRequest = await fetchTaskRequest();
+ taskRequestSkeleton.classList.add('hidden');
+ renderRejectButton(taskRequest);
+ renderTaskRequestDetails(taskRequest);
+
+ if (taskRequest?.requestType === 'CREATION') {
+ renderGithubIssue();
+ } else if (taskRequest?.requestType === 'ASSIGNMENT') {
+ renderTaskDetails(taskRequest);
+ }
+ renderRequestors(taskRequest);
+ } catch (e) {
+ console.error(e);
+ }
+};
+
+function showToast(message, type) {
+ toast.innerHTML = `${message}
`;
+ toast.classList.remove('hidden');
+
+ if (type === 'success') {
+ toast.classList.add('success');
+ toast.classList.remove('failure');
+ } else if (type === 'failure') {
+ toast.classList.add('failure');
+ toast.classList.remove('success');
+ }
+
+ setTimeout(() => {
+ toast.classList.add('hidden');
+ toast.innerHTML = '';
+ }, 5000);
+}
+
+async function getAssigneeName() {
+ let userName = '';
+ let res;
+ if (taskRequest.approvedTo) {
+ try {
+ res = await fetch(
+ `${API_BASE_URL}/users/userId/${taskRequest.approvedTo}`,
+ );
+ } catch (error) {
+ console.error(error);
+ }
+ if (res.ok) {
+ const userData = await res.json();
+ userName = userData.user.first_name;
+ }
+ }
+ return userName;
+}
+
+async function renderAssignedTo(userName) {
+ const assignedToText = 'Assigned To: ';
+ const linkOrText = userName.length
+ ? `${userName}`
+ : 'N/A';
+
+ taskContainer.append(
+ createCustomElement({
+ tagName: 'p',
+ class: 'task__createdBy',
+ id: 'task__createdBy',
+ innerHTML: assignedToText + linkOrText,
+ }),
+ );
+}
+
+const openModalBtn = document.getElementById('requestor_details_modal_open');
+const closeModal = document.getElementById('requestor_details_modal_close');
+
+const modalOverlay = document.getElementById('overlay');
+
+closeModal.addEventListener('click', function () {
+ modalOverlay.style.display = 'none';
+});
+modalOverlay.addEventListener('click', function (event) {
+ if (event.target == modalOverlay) {
+ modalOverlay.style.display = 'none';
+ }
+});
+
+function populateModalContent(index) {
+ if (
+ !Array.isArray(taskRequest.users) ||
+ index < 0 ||
+ index >= taskRequest.users.length
+ ) {
+ showToast('No Data Available for this requestor', 'failure');
+ return;
+ }
+ const modal = document.getElementById('requestor_details_modal_content');
+ const userData = taskRequest.users[index];
+
+ const modalContent = modal.querySelector('.requestor_details_modal_info');
+
+ const proposedStartDateText = document.createElement('p');
+ proposedStartDateText.setAttribute(
+ 'data-modal-start-date-text',
+ 'proposed-start-date-text',
+ );
+ proposedStartDateText.innerHTML = 'Proposed Start Date:';
+
+ const proposedStartDateValue = document.createElement('p');
+ proposedStartDateValue.setAttribute(
+ 'data-modal-start-date-value',
+ 'proposed-start-date-value',
+ );
+ proposedStartDateValue.textContent = getHumanReadableDate(
+ userData.proposedStartDate,
+ );
+
+ const proposedDeadlineText = document.createElement('p');
+ proposedDeadlineText.setAttribute(
+ 'data-modal-end-date-text',
+ 'proposed-end-date-text',
+ );
+ proposedDeadlineText.innerHTML = 'Proposed Deadline:';
+
+ const proposedDeadlineValue = document.createElement('p');
+ proposedDeadlineValue.setAttribute(
+ 'data-modal-end-date-value',
+ 'proposed-end-date-value',
+ );
+ proposedDeadlineValue.textContent = getHumanReadableDate(
+ userData.proposedDeadline,
+ );
+
+ const descriptionText = document.createElement('p');
+ descriptionText.setAttribute(
+ 'data-modal-description-text',
+ 'proposed-description-text',
+ );
+ descriptionText.innerHTML = 'Description:';
+
+ const descriptionValue = document.createElement('p');
+ descriptionValue.setAttribute(
+ 'data-modal-description-value',
+ 'proposed-description-value',
+ );
+ descriptionValue.textContent = userData.description;
+
+ const header = document.createElement('h2');
+ header.setAttribute('data-modal-header', 'requestor-details-header');
+ header.className = 'requestor_details_modal_heading';
+ header.textContent = 'Requestor Details';
+
+ modalContent.innerHTML = '';
+
+ modalContent.appendChild(header);
+ modalContent.appendChild(proposedStartDateText);
+ modalContent.appendChild(proposedStartDateValue);
+ modalContent.appendChild(proposedDeadlineText);
+ modalContent.appendChild(proposedDeadlineValue);
+ modalContent.appendChild(descriptionText);
+ modalContent.appendChild(descriptionValue);
+ modalOverlay.style.display = 'block';
+}
+
+renderTaskRequest();
diff --git a/taskRequests/details/style.css b/taskRequests/details/style.css
new file mode 100644
index 00000000..9d35d32a
--- /dev/null
+++ b/taskRequests/details/style.css
@@ -0,0 +1,522 @@
+:root {
+ font-family: 'Inter', sans-serif;
+ --color-success: rgba(20, 102, 75, 0.6);
+ --color-gray-light: #eee;
+ --color-gray: #666;
+ --color-green: green;
+ --color-warn: rgba(199, 129, 18, 0.4);
+ --color-warn-background: #fcf1e0;
+ --color-white: white;
+ --color-rds-blue: #1d1283;
+ --color-blue-light: #1d1283af;
+ --color-black: #000;
+ --color-gray-shade: #aaa;
+ --color-red-variant1: #f43030;
+ --color-black-shade-70percent: rgba(0, 0, 0, 0.7);
+ --color-light-gray: #f4f4f4;
+ --color-gray-variant2: #888;
+ --color-red-light: #fadee0;
+ --color-red-variant2: #ae1820;
+}
+
+body {
+ padding: 0;
+ margin: 0;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.skeleton {
+ animation: skeleton 2s linear infinite;
+ border-radius: 0.5rem;
+ min-height: 0.5rem;
+ margin: 0.5rem 0;
+}
+
+.header {
+ background: #1d1283;
+ padding: 1rem;
+}
+.header__contents {
+ max-width: 1440px;
+ padding: 0.5rem 1rem;
+ margin: 0 auto;
+ color: var(--color-white);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+.header__contents__navlink {
+ color: var(--color-white);
+ text-decoration: none;
+}
+.header__contents__navlink:hover {
+ text-decoration: underline;
+}
+
+.container {
+ max-width: 1440px;
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+}
+.container-disabled {
+ opacity: 50%;
+ pointer-events: none;
+}
+
+.request-details {
+ grid-column: auto / span 4;
+}
+
+.taskRequest {
+ padding: 1rem;
+ grid-column: 1 / span 12;
+}
+.taskRequest__skeleton__title {
+ height: 1.5rem;
+ width: 50ch;
+ margin: 0.5rem 0;
+}
+.taskRequest__skeleton__subtitle {
+ height: 1rem;
+ max-width: 30ch;
+ animation: skeleton 2s linear infinite;
+}
+.taskRequest__title {
+ font-weight: 400;
+ font-size: 2rem;
+ line-height: 2.5rem;
+}
+.taskRequest__title__subtitle {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--color-gray-variant2);
+ font-size: 0.875rem;
+}
+.taskRequest__status__chip {
+ padding: 0.5rem;
+ line-height: 1.5rem;
+ border-radius: 1rem;
+ font-weight: 700;
+}
+.taskRequest__status__chip--approved {
+ background: #e1f9f1;
+ color: #19805e;
+}
+.taskRequest__status__chip--waiting {
+ background: #fcf1e0;
+ color: #c78112;
+}
+.taskRequest__status__chip--pending {
+ background: var(--color-warn-background);
+ color: var(--color-warn);
+}
+.taskRequest__status__chip--denied {
+ background: var(--color-red-light);
+ color: var(--color-red-variant2);
+}
+.taskRequest__status__chip--tag {
+ background: var(--color-gray-light);
+ color: var(--color-gray);
+}
+.task__skeleton__title {
+ height: 1.25rem;
+ max-width: 45ch;
+}
+.task__skeleton__details {
+ height: 0.75rem;
+ max-width: 20ch;
+}
+.task__skeleton__description {
+ height: 0.75rem;
+ max-width: 75ch;
+}
+
+.task {
+ grid-column: 1 / span 8;
+ padding: 1rem;
+}
+.task__title {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ color: #1d1283;
+ margin: 0;
+}
+.task__purpose {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ margin-top: 1rem;
+ max-width: 80ch;
+}
+.task__type__chip {
+ padding: 0.5rem;
+ line-height: 1.5rem;
+ border-radius: 1rem;
+ font-weight: 700;
+ margin: 0 0.25rem;
+ white-space: nowrap;
+}
+.task__type__chip--feature {
+ background: #dfe4ff;
+ border: solid 1px #9eadfe;
+ color: #0224df;
+}
+.task__type__chip--refactor {
+ background: #fadee0;
+ border: solid 1px #f19ca1;
+ color: #ae1820;
+}
+.task__type__chip--bug {
+ background: #e1f9f1;
+ border: solid 1px #7fe6c4;
+ color: #14664b;
+}
+.task__type__chip--noteworthy {
+ background: #14664b;
+ color: var(--color-white);
+}
+
+.requestors {
+ padding: 1rem;
+ align-self: flex-start;
+}
+.requestors__container__title {
+ font-size: 1.375rem;
+ line-height: 1.75rem;
+ font-weight: 400;
+ margin: 0;
+}
+.requestors__container__list {
+ list-style-type: none;
+ padding: 0;
+}
+.requestors__container__list li {
+ padding: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.requestors__container__list__userDetails {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+.requestors__container__list__userDetails__avatar {
+ height: 2rem;
+ width: 2rem;
+ display: grid;
+ background-color: #e2e2e2;
+ place-items: center;
+ border-radius: 50%;
+}
+.requestors__container__list li:nth-child(even) {
+ background: #eee;
+}
+.info__name {
+ margin: 0;
+}
+.info__more {
+ margin: 0;
+ margin-top: 2px;
+ display: block;
+ width: 100%;
+ text-align: end;
+ font-size: 0.7rem;
+ color: var(--color-rds-blue);
+ cursor: pointer;
+}
+.info__more:hover {
+ color: var(--color-blue-light);
+}
+.requestors__conatainer__list__button {
+ padding: 0.375rem 0.5rem;
+ background: #fff;
+ border: solid 1px #19805e;
+ font-weight: 700;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ color: #19805e;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ margin-left: 1rem;
+}
+.requestors__conatainer__list__button:hover {
+ color: var(--color-white);
+ background: #19805e;
+ transition: 0.3s ease-in-out;
+}
+.requestors__container__list__approved {
+ background: transparent;
+ border: none;
+ color: #c3c3c3;
+ font-weight: 600;
+ margin-left: 1rem;
+}
+.reject__container {
+ display: flex;
+ justify-content: end;
+ margin-top: 2.5rem;
+ padding: 1rem;
+ min-width: 8rem;
+}
+.request-details__reject__button {
+ padding: 0.375rem 0.5rem;
+ width: 80%;
+ max-width: 10rem;
+ background: var(--color-white);
+ border: solid 1px var(--color-red-variant1);
+ font-weight: 700;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ color: var(--color-red-variant1);
+ border-radius: 0.25rem;
+ cursor: pointer;
+}
+.request-details__reject__button:hover {
+ color: var(--color-white);
+ background: var(--color-red-variant1);
+ transition: 0.3s ease-in-out;
+}
+.request-details__reject__button:disabled {
+ color: var(--color-gray-variant2);
+ background: transparent;
+ border: solid 1px var(--color-gray-variant2);
+ pointer-events: none;
+}
+
+.circular-image {
+ border-radius: 50%;
+ max-width: 100%;
+ max-height: 100%;
+}
+
+.success {
+ color: var(--color-white);
+ background: var(--color-green);
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+}
+
+.failure {
+ color: var(--color-white);
+ background: var(--color-red-variant1);
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+}
+.spinner {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 2.5rem;
+ height: 2.5rem;
+ border: 9px solid transparent;
+ border-top: 9px solid var(--color-rds-blue);
+ border-radius: 50%;
+ z-index: 100;
+ animation: spin 1s linear infinite;
+}
+#toast_task_details {
+ position: absolute;
+ top: 90%;
+ right: 10%;
+ padding: 10px;
+ border-radius: 5px;
+ font-weight: bold;
+ border: 1px solid;
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+}
+
+@-webkit-keyframes spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes skeleton {
+ 0% {
+ background: hsl(0, 0%, 75%);
+ }
+ 50% {
+ background: hsl(0, 0%, 95%);
+ }
+ 100% {
+ background: hsl(0, 0%, 75%);
+ }
+}
+#task-details img {
+ max-width: 100%;
+ border: 1px solid #78716c;
+ border-radius: 0.5rem;
+}
+#task-details p a {
+ word-wrap: break-word;
+}
+#task-details table {
+ background: #fff;
+ border: solid 1px #ddd;
+ margin-bottom: 1.25rem;
+ table-layout: auto;
+ border-spacing: 2px;
+}
+#task-details table thead {
+ background: #f5f5f5;
+}
+#task-details table thead tr th,
+#task-details table thead tr td {
+ color: #222;
+ font-size: 0.875rem;
+ font-weight: bold;
+ padding: 0.5rem 0.625rem 0.625rem;
+}
+#task-details table thead tr th,
+#task-details table tfoot tr th,
+#task-details table tfoot tr td,
+#task-details table tbody tr th,
+#task-details table tbody tr td,
+#task-details table tr td {
+ display: table-cell;
+ line-height: 1.125rem;
+}
+#task-details table tr th,
+table tr td {
+ color: #222;
+ font-size: 0.875rem;
+ padding: 0.5625rem 0.625rem;
+ text-align: left;
+}
+.card__link {
+ color: #2563eb;
+ text-decoration: none;
+}
+.card__link_issue {
+ color: #78716c;
+}
+
+.card__tag {
+ display: block;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8rem;
+ border: none;
+ border-radius: 1.25rem;
+ background: #dbeafe;
+ font-weight: 500;
+ font-family: Inter;
+ margin-right: 0.5rem;
+}
+.task__issue__container {
+ margin: 0 1rem 1.5rem 1rem;
+ border-right: solid 1px rgba(0, 0, 0, 0.1);
+ border: solid 1px rgba(0, 0, 0, 0.1);
+ border-radius: 0.5rem;
+}
+
+.requester-border {
+ border-left: solid 1px rgba(0, 0, 0, 0.1);
+}
+
+@media (max-width: 599px) {
+ .taskRequest__title {
+ font-size: 1.5rem;
+ line-height: 1.75rem;
+ }
+ .taskRequest__title__subtitle {
+ font-size: 0.875rem;
+ line-height: 1rem;
+ }
+ .taskRequest__status {
+ font-size: 0.75rem;
+ }
+}
+
+@media (max-width: 904px) {
+ .task {
+ grid-column: 1 / span 12;
+ }
+
+ .requestors {
+ grid-column: 1 / span 12;
+ border: none;
+ }
+
+ .taskRequest__skeleton__title {
+ max-width: 80%;
+ height: 1rem;
+ }
+ .taskRequest__skeleton__subtitle {
+ max-width: 40%;
+ }
+ .request-details {
+ grid-column: auto;
+ }
+}
+
+.requestor_details_modal_content {
+ background-color: var(--color-light-gray);
+ margin: 10% auto;
+ padding: 2rem;
+ border: 1px solid var(--color-gray-variant2);
+ width: 25%;
+ text-align: center;
+ right: 10%;
+ border-radius: 1rem;
+}
+
+.requestor_details_modal_close {
+ color: var(--color-gray-shade);
+ float: right;
+ font-size: 28px;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.requestor_details_modal_close:hover {
+ color: var(--color-black);
+}
+
+.requestor_details_modal_textarea {
+ width: 80%;
+ min-width: 50%;
+ max-width: 100%;
+}
+
+.requestor_details_modal_heading {
+ color: var(--color-rds-blue);
+}
+
+.requestors__container__list__userDetails__avatar,
+.requestors__container__list__userDetails__avatar:hover,
+p:hover {
+ cursor: pointer;
+}
+
+.overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: none;
+}
diff --git a/taskRequests/index.html b/taskRequests/index.html
new file mode 100644
index 00000000..ba7a233a
--- /dev/null
+++ b/taskRequests/index.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+ Task Requests | Real Dev Squad
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Filters
+
+
+
+
Status
+
+ Request type
+
+
+
+
+
+
+
+
+
diff --git a/taskRequests/script.js b/taskRequests/script.js
new file mode 100644
index 00000000..c8e2c26c
--- /dev/null
+++ b/taskRequests/script.js
@@ -0,0 +1,420 @@
+const API_BASE_URL = window.API_BASE_URL;
+const taskRequestContainer = document.getElementById('task-request-container');
+const containerBody = document.querySelector('.container__body');
+const filterModal = document.getElementsByClassName(FILTER_MODAL)[0];
+const applyFilterButton = document.getElementById(APPLY_FILTER_BUTTON);
+const clearButton = document.getElementById(CLEAR_BUTTON);
+const filterButton = document.getElementById(FILTER_BUTTON);
+const sortModal = document.getElementsByClassName(SORT_MODAL)[0];
+const containerFilter = document.querySelector(FILTER_CONTAINER);
+const lastElementContainer = document.querySelector(LAST_ELEMENT_CONTAINER);
+const sortButton = document.querySelector(SORT_BUTTON);
+const backDrop = document.querySelector(BACKDROP);
+const params = new URLSearchParams(window.location.search);
+const isDev = params.get(DEV_FEATURE_FLAG) === 'true';
+const loader = document.querySelector('.container__body__loader');
+const startLoading = () => loader.classList.remove('hidden');
+const stopLoading = () => loader.classList.add('hidden');
+let pageVersion = 0;
+let nextLink = '';
+let isDataLoading = false;
+let selectedSortButton = null;
+
+const filterStates = {
+ dev: true,
+ status: Status.PENDING,
+ order: CREATED_TIME,
+ size: DEFAULT_PAGE_SIZE,
+};
+
+const updateFilterStates = (key, value) => {
+ filterStates[key] = value;
+};
+
+async function getTaskRequests(query = {}, nextLink) {
+ let finalUrl =
+ API_BASE_URL + (nextLink || '/taskRequests' + getQueryParamsString(query));
+ try {
+ const res = await fetch(finalUrl, {
+ credentials: 'include',
+ });
+
+ if (res.ok) {
+ const data = await res.json();
+ return data;
+ }
+
+ if (res.status === 401) {
+ showMessage('ERROR', ErrorMessages.UNAUTHENTICATED);
+ return;
+ }
+
+ if (res.status === 403) {
+ showMessage('ERROR', ErrorMessages.UNAUTHORIZED);
+ return;
+ }
+
+ if (res.status === 404) {
+ showMessage('ERROR', ErrorMessages.NOT_FOUND);
+ return;
+ }
+
+ showMessage('ERROR', ErrorMessages.SERVER_ERROR);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+function showMessage(type, message) {
+ const p = document.createElement('p');
+ const classes = ['taskRequest__message'];
+ if (type === 'ERROR') {
+ classes.push('taskRequest__message--error');
+ }
+ p.classList.add(...classes);
+ p.textContent = message;
+ taskRequestContainer.innerHTML = '';
+ taskRequestContainer.appendChild(p);
+}
+
+function getAvatar(user) {
+ if (user?.picture?.url) {
+ return createCustomElement({
+ tagName: 'img',
+ src: user?.picture?.url,
+ });
+ }
+ return createCustomElement({
+ tagName: 'span',
+ textContent: user?.first_name?.[0] || '?',
+ });
+}
+function getRemainingCount(requestors) {
+ if (requestors.length > 3) {
+ return createCustomElement({
+ tagName: 'span',
+ textContent: `+${requestors.length - 3}`,
+ });
+ }
+}
+function openTaskDetails(id) {
+ const url = new URL(`/taskRequests/details`, window.location.href);
+
+ url.searchParams.append('id', id);
+ window.location.href = url;
+}
+const changeFilter = () => {
+ nextLink = '';
+ taskRequestContainer.innerHTML = '';
+};
+
+sortButton.addEventListener('click', async (event) => {
+ event.stopPropagation();
+ sortModal.classList.toggle('hidden');
+ backDrop.style.display = 'flex';
+});
+
+backDrop.addEventListener('click', () => {
+ sortModal.classList.add('hidden');
+ filterModal.classList.add('hidden');
+ backDrop.style.display = 'none';
+});
+
+function toggleStatusCheckbox(statusValue) {
+ const element = document.querySelector(
+ `#status-filter input[value=${statusValue}]`,
+ );
+ element.checked = !element.checked;
+}
+function clearCheckboxes(groupName) {
+ const checkboxes = document.querySelectorAll(`input[name="${groupName}"]`);
+ checkboxes.forEach((cb) => {
+ cb.checked = false;
+ });
+}
+function getCheckedValues(groupName) {
+ const checkboxes = document.querySelectorAll(
+ `input[name="${groupName}"]:checked`,
+ );
+ return Array.from(checkboxes).map((cb) => cb.value.toLowerCase());
+}
+
+filterButton.addEventListener('click', (event) => {
+ filterModal.classList.toggle('hidden');
+ backDrop.style.display = 'flex';
+});
+
+applyFilterButton.addEventListener('click', async () => {
+ filterModal.classList.toggle('hidden');
+ const checkedValuesStatus = getCheckedValues('status-filter');
+ const checkedValuesRequestType = getCheckedValues('request-type-filter');
+ changeFilter();
+ if (checkedValuesStatus) {
+ updateFilterStates('status', checkedValuesStatus);
+ }
+ if (checkedValuesRequestType) {
+ updateFilterStates('requestType', checkedValuesRequestType);
+ }
+ await renderTaskRequestCards(filterStates);
+});
+clearButton.addEventListener('click', async function () {
+ clearCheckboxes('status-filter');
+ filterModal.classList.toggle('hidden');
+ changeFilter();
+ updateFilterStates('status', '');
+ await renderTaskRequestCards(filterStates);
+});
+
+function addCheckbox(labelText, value, groupName) {
+ const group = document.getElementById(groupName);
+ const label = document.createElement('label');
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.name = groupName;
+ checkbox.value = value;
+ label.innerHTML = checkbox.outerHTML + ' ' + labelText;
+ label.classList.add('checkbox-label');
+ label.appendChild(document.createElement('br'));
+ group.appendChild(label);
+}
+function addSortByIcon(name, id, groupName, order) {
+ const group = document.getElementById(groupName);
+
+ const containerAsc = createSortContainer(id, name, order);
+ group.appendChild(containerAsc);
+}
+
+function sortModalButtons() {
+ const assigneeAsc = document.getElementById(ASSIGNEE_COUNT);
+ const assigneeDesc = document.getElementById(ASSIGNEE_DESC);
+ const createTimeAsc = document.getElementById(CREATED_TIME);
+ const createTimeDesc = document.getElementById(CREATED_TIME_DESC);
+
+ const sortModalButtons = [
+ assigneeAsc,
+ assigneeDesc,
+ createTimeAsc,
+ createTimeDesc,
+ ];
+
+ function toggleSortModal() {
+ sortModal.classList.toggle('hidden');
+ backDrop.style.display = 'none';
+ }
+
+ function selectButton(button) {
+ if (selectedSortButton === button) {
+ selectedSortButton.classList.remove('selected');
+ selectedSortButton = null;
+ toggleSortModal();
+ } else {
+ if (selectedSortButton) {
+ selectedSortButton.classList.remove('selected');
+ }
+ selectedSortButton = button;
+ selectedSortButton.classList.add('selected');
+ toggleSortModal();
+ }
+ }
+
+ sortModalButtons.forEach((button) => {
+ if (button) {
+ button.addEventListener('click', async () => {
+ selectButton(button);
+ changeFilter();
+ updateFilterStates('order', button.id);
+ await renderTaskRequestCards(filterStates);
+ });
+ }
+ });
+ selectButton(createTimeAsc);
+ toggleSortModal();
+}
+
+function createSortContainer(id, name, sortOrder) {
+ const container = document.createElement('div');
+ container.classList.add('sort-container', sortOrder);
+
+ container.id = id;
+
+ const nameSpan = document.createElement('span');
+ nameSpan.classList.add('sort__button__text');
+ nameSpan.textContent = name;
+ const label = document.createElement('label');
+ label.appendChild(nameSpan);
+
+ label.classList.add('sort-label');
+
+ container.appendChild(label);
+
+ return container;
+}
+
+function populateStatus() {
+ const statusList = [
+ { name: 'Approved', id: 'APPROVED' },
+ { name: 'Pending', id: 'PENDING' },
+ { name: 'Denied', id: 'DENIED' },
+ ];
+ const requestList = [
+ { name: 'Assignment', id: 'assignment' },
+ { name: 'Creation', id: 'creation' },
+ ];
+
+ statusList.map(({ name, id }) => addCheckbox(name, id, 'status-filter'));
+
+ requestList.map(({ name, id }) =>
+ addCheckbox(name, id, 'request-type-filter'),
+ );
+
+ const sortByList = [
+ {
+ name: 'Least Requested',
+ id: 'REQUESTORS_COUNT_ASC',
+ order: 'asc',
+ },
+ {
+ name: 'Most Requested',
+ id: 'REQUESTORS_COUNT_DESC',
+ order: 'desc',
+ },
+ {
+ name: 'Newest First',
+ id: 'CREATED_TIME_DESC',
+ order: 'desc',
+ },
+ {
+ name: 'Oldest First',
+ id: 'CREATED_TIME_ASC',
+ order: 'asc',
+ },
+ ];
+
+ sortByList.forEach(({ name, id, order }) =>
+ addSortByIcon(name, id, 'sort_by-filter', order),
+ );
+}
+
+populateStatus();
+sortModalButtons();
+
+function createTaskRequestCard(taskRequest) {
+ let { id, task, status, taskTitle, users } = taskRequest;
+ const card = createCustomElement({
+ tagName: 'div',
+ class: 'taskRequest__card',
+ eventListeners: [{ event: 'click', func: (e) => openTaskDetails(id, e) }],
+ child: [
+ createCustomElement({
+ tagName: 'div',
+ class: 'taskRequest__card__header',
+ child: [
+ createCustomElement({
+ tagName: 'h3',
+ class: 'taskRequest__card__header__title',
+ textContent: task?.title || taskTitle,
+ }),
+ createCustomElement({
+ tagName: 'div',
+ class: [
+ 'taskRequest__card__header__status',
+ `taskRequest__card__header__status--${status.toLowerCase()}`,
+ ],
+ title: status.toLowerCase(),
+ }),
+ ],
+ }),
+ createCustomElement({
+ tagName: 'div',
+ class: 'taskRequest__card__body',
+ child: [
+ createCustomElement({
+ tagName: 'p',
+ textContent: task?.purpose,
+ }),
+ ],
+ }),
+ createCustomElement({
+ tagName: 'div',
+ class: 'taskRequest__card__footer',
+ child: [
+ createCustomElement({
+ tagName: 'p',
+ textContent: 'Requested By',
+ }),
+ createCustomElement({
+ tagName: 'div',
+ class: 'taskRequest__card__footer__requestor',
+ child: [
+ ...users.map((user, index) => {
+ if (index < 3) {
+ return createCustomElement({
+ tagName: 'div',
+ class: 'taskRequest__card__footer__requestor__avatar',
+ title: user?.first_name,
+ child: [getAvatar(user)],
+ });
+ }
+ }),
+ getRemainingCount(users) || '',
+ ],
+ }),
+ ],
+ }),
+ ],
+ });
+ return card;
+}
+
+const intersectionObserver = new IntersectionObserver(async (entries) => {
+ if (!nextLink) {
+ return;
+ }
+ if (entries[0].isIntersecting && !isDataLoading) {
+ await renderTaskRequestCards({}, nextLink);
+ }
+});
+
+const addIntersectionObserver = () => {
+ intersectionObserver.observe(lastElementContainer);
+};
+const removeIntersectionObserver = () => {
+ intersectionObserver.unobserve(lastElementContainer);
+};
+
+async function renderTaskRequestCards(queries = {}, newLink = '') {
+ pageVersion++;
+ const currentVersion = pageVersion;
+ try {
+ isDataLoading = true;
+ startLoading();
+ const taskRequestResponse = await getTaskRequests(queries, newLink);
+ const taskRequestsList = taskRequestResponse.data;
+ nextLink = taskRequestResponse.next;
+ if (currentVersion !== pageVersion) {
+ return;
+ }
+ taskRequestsList.forEach((taskRequest) => {
+ taskRequestContainer.appendChild(createTaskRequestCard(taskRequest));
+ });
+ } catch (error) {
+ console.error(error);
+ showMessage('ERROR', ErrorMessages.SERVER_ERROR);
+ } finally {
+ if (currentVersion !== pageVersion) return;
+ stopLoading();
+ isDataLoading = false;
+ if (taskRequestContainer.innerHTML === '') {
+ showMessage('INFO', 'No task requests found!');
+ }
+ }
+}
+
+async function render() {
+ toggleStatusCheckbox(Status.PENDING.toUpperCase());
+
+ await renderTaskRequestCards(filterStates);
+ addIntersectionObserver();
+}
+
+render();
diff --git a/taskRequests/style.css b/taskRequests/style.css
new file mode 100644
index 00000000..8d5ae76c
--- /dev/null
+++ b/taskRequests/style.css
@@ -0,0 +1,428 @@
+:root {
+ font-family: 'Inter', sans-serif;
+ --color-primary: #1d1283;
+ --color-success: rgba(20, 102, 75, 0.6);
+ --color-error: #da1e28;
+ --color-warn: rgba(199, 129, 18, 0.4);
+ --color-gray-light: #eee;
+ --color-gray: #666;
+ --red-color: red;
+ --light-gray-color: lightgray;
+ --dark-gray-color: rgb(199, 195, 195);
+ --blue-hover-color: #11085c;
+ --black-color: black;
+ --blue-color: #1d1283;
+ --white: #ffffff;
+ --color-text-light: rgba(0, 0, 0, 0.6);
+ --elevation-1: 0 1px 3px 1px rgba(0, 0, 0, 0.1),
+ 0 1px 2px 0 rgba(0, 0, 0, 0.1);
+ --elevation-3: 0px 1px 3px 0px rgba(0, 0, 0, 0.3),
+ 0px 4px 8px 3px rgba(0, 0, 0, 0.15);
+ --black-transparent: #000000a8;
+ --light-gray-color: lightgray;
+}
+
+body {
+ padding: 0;
+ margin: 0;
+}
+
+.hidden {
+ display: none;
+}
+
+.header {
+ background: var(--color-primary);
+ padding: 1rem;
+}
+.header__contents {
+ max-width: 1440px;
+ padding: 0.5rem 1rem;
+ margin: 0 auto;
+ color: white;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+.header__contents__navlink {
+ color: white;
+ text-decoration: none;
+}
+.header__contents__navlink:hover {
+ text-decoration: underline;
+}
+
+.container {
+ max-width: 1440px;
+ margin: 0 auto;
+ padding: 2.5rem;
+ position: relative;
+}
+.container__filters {
+ margin: 1rem 0;
+ display: none;
+ gap: 0.5rem;
+}
+.container__filters__status {
+ border-radius: 0.5rem;
+ min-width: 8rem;
+ padding: 0.5rem 0.25rem;
+ background: white;
+ font-size: 1rem;
+ line-height: 1.25rem;
+}
+.container__title {
+ font-weight: 400;
+ font-size: 2rem;
+ line-height: 2.5rem;
+}
+.container__body__loader {
+ font-weight: 600;
+ text-align: center;
+ font-size: 1.5rem;
+ margin: 2rem;
+}
+.container__body {
+ margin-top: 2.5rem;
+}
+.sort-filters {
+ display: flex;
+ justify-content: end;
+ align-items: center;
+ gap: 1rem;
+}
+
+.funnel-icon {
+ width: 1.2rem;
+ height: 1.5rem;
+ margin-left: 0.5rem;
+}
+
+.filter-button:hover {
+ background-color: var(--blue-hover-color);
+}
+.filter-button {
+ background-color: var(--blue-color);
+ color: var(--white);
+ border: none;
+ border-radius: 0.4rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 9rem;
+ height: 2.5rem;
+ padding: 0.7rem;
+}
+
+.sort-button {
+ display: flex;
+ background-color: var(--light-gray-color);
+ border-radius: 0.4rem;
+ border: 2.5px solid var(--black-color);
+ padding: 12px;
+ height: 2.5rem;
+ cursor: pointer;
+}
+
+.sort__button__text {
+ cursor: pointer;
+}
+.sort-button:hover {
+ background-color: var(--medium-gray);
+}
+
+.sort-modal {
+ width: 20%;
+ min-width: 14rem;
+ max-width: 18rem;
+ border: 1px solid var(--light-gray-color);
+ box-shadow: 0 0 10px var(--black-transparent);
+ border-radius: 0.31rem;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem;
+ padding-bottom: 1.5rem;
+ z-index: 2;
+ position: absolute;
+ top: 5.5rem;
+ background-color: var(--white);
+}
+
+.sort-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 6px;
+ padding: 0.5rem;
+ border: 1px solid var(--light-gray-color);
+ border-radius: 0.31rem;
+ cursor: pointer;
+ font-size: 0.9rem;
+}
+.selected {
+ background-color: var(--dark-gray-color);
+}
+
+.sort-container:hover {
+ background-color: var(--color-gray-light);
+}
+
+.sort-label {
+ display: contents;
+}
+
+/* Filter modal */
+.filter-modal {
+ width: 20%;
+ min-width: 14rem;
+ max-width: 18rem;
+ border: 1px solid var(--light-gray-color);
+ box-shadow: 0 0 10px var(--black-transparent);
+ border-radius: 0.31rem;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem;
+ padding-bottom: 1.5rem;
+ position: absolute;
+ top: 5.5rem;
+ background-color: var(--white);
+}
+
+.filter-head,
+.sort-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 1rem 0 1rem;
+}
+
+.filters-container,
+.sorts-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+}
+
+.modal-form {
+ text-align: initial;
+ padding: 0.5rem;
+ width: 80%;
+}
+
+.checkbox-label {
+ display: block;
+ margin-bottom: 0.5rem;
+}
+
+.clear-btn {
+ background-color: var(--white);
+ border: 1px solid var(--light-gray-color);
+ border-radius: 0.31rem;
+ padding: 0.31rem 0.62rem;
+ cursor: pointer;
+}
+
+.clear-btn:hover {
+ background-color: var(--red-color);
+ color: var(--white);
+}
+
+.filters {
+ width: 100%;
+ padding: 0.62rem;
+ border: 1px solid var(--light-gray-color);
+ border-radius: 0.31rem;
+ margin: 0.31rem 0.62rem;
+ cursor: pointer;
+}
+
+.filters:hover {
+ background-color: var(--light-gray-color);
+ border: 1px solid var(--black-color);
+}
+
+.apply-filter-button {
+ border: 1px solid var(--light-gray-color);
+ border-radius: 0.31rem;
+ padding: 0.62rem;
+ cursor: pointer;
+ width: 100%;
+ background-color: var(--blue-color);
+ color: var(--white);
+}
+
+.apply-filter-button:hover {
+ background-color: var(--blue-hover-color);
+}
+
+/* Filter modal end */
+
+.backdrop {
+ display: none;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.taskRequest {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 1rem;
+}
+.taskRequest__message {
+ line-height: 1.25rem;
+ font-weight: 600;
+ text-align: center;
+ font-size: 1.5rem;
+ width: 100%;
+ position: absolute;
+}
+.taskRequest__message--error {
+ color: var(--color-error);
+ font-weight: 600;
+ text-align: center;
+ font-size: 1.5rem;
+}
+.taskRequest__card {
+ cursor: pointer;
+ padding: 1rem;
+ box-shadow: var(--elevation-1);
+ border-radius: 0.5rem;
+ min-width: 16rem;
+ display: flex;
+ flex-flow: column;
+ gap: 0.5rem;
+}
+.taskRequest__card:hover {
+ box-shadow: var(--elevation-3);
+ transition: 300ms ease-in;
+}
+.taskRequest__card:active {
+ box-shadow: var(--elevation-1);
+ transition: 100ms ease-in;
+}
+.taskRequest__card__header {
+ display: flex;
+ justify-content: space-between;
+}
+.taskRequest__card__header__title {
+ margin: 0;
+ font-weight: 400;
+ font-size: 1.5rem;
+ line-height: 2rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 90%;
+ color: var(--color-primary);
+}
+.taskRequest__card__header__status {
+ height: 0.75rem;
+ width: 0.75rem;
+ border-radius: 50%;
+}
+.taskRequest__card__header__status--approved {
+ background-color: var(--color-success);
+}
+.taskRequest__card__header__status--waiting {
+ background-color: var(--color-warn);
+}
+.taskRequest__card__header__status--pending {
+ background-color: var(--color-warn);
+}
+.taskRequest__card__header__status--denied {
+ background-color: var(--red-color);
+}
+.taskRequest__card__body p {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ margin: 0;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ overflow: hidden;
+ color: var(--color-text-light);
+}
+.taskRequest__card__footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.taskRequest__card__footer__requestor {
+ display: flex;
+ margin-left: 0.75rem;
+}
+.taskRequest__card__footer__requestor__avatar {
+ display: grid;
+ place-items: center;
+ height: 2rem;
+ width: 2rem;
+ margin-left: -0.75rem;
+ border: solid 2px white;
+ overflow: hidden;
+ border-radius: 50%;
+ background-color: var(--color-gray-light);
+}
+.taskRequest__card__footer__requestor__avatar img {
+ height: 100%;
+ width: 100%;
+}
+.taskRequest__card__footer__requestor__avatar span {
+ font-size: 0.875rem;
+ color: var(--color-gray);
+}
+
+@media screen and (max-width: 440px) {
+ .container {
+ padding: 1.5rem;
+ }
+
+ .filter-button {
+ width: min-content;
+ }
+
+ .filter-text {
+ display: none;
+ }
+
+ .filter-modal,
+ .sort-modal {
+ top: 4.5rem;
+ }
+ .funnel-icon {
+ margin: auto;
+ }
+}
+
+@media screen and (max-width: 320px) {
+ .container {
+ padding: 1rem;
+ }
+
+ .filter-modal,
+ .sort-modal {
+ top: 4rem;
+ }
+}
+@media (max-width: 1439px) {
+ .taskRequest {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+@media (max-width: 905px) {
+ .taskRequest {
+ grid-template-columns: 1fr 1fr;
+ }
+}
+@media (max-width: 599px) {
+ .container__filters {
+ justify-content: space-between;
+ }
+ .taskRequest {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/taskRequests/util.js b/taskRequests/util.js
new file mode 100644
index 00000000..d3e55bb2
--- /dev/null
+++ b/taskRequests/util.js
@@ -0,0 +1,69 @@
+function createCustomElement(domObjectMap) {
+ const el = document.createElement(domObjectMap.tagName);
+ for (const [key, value] of Object.entries(domObjectMap)) {
+ if (key === 'tagName') {
+ continue;
+ }
+ if (key === 'eventListeners') {
+ value.forEach((obj) => {
+ el.addEventListener(obj.event, obj.func);
+ });
+ }
+ if (key === 'class') {
+ if (Array.isArray(value)) {
+ el.classList.add(...value);
+ } else {
+ el.classList.add(value);
+ }
+ } else if (key === 'child') {
+ el.append(...value);
+ } else {
+ el[key] = value;
+ }
+ }
+ return el;
+}
+
+function getQueryParamsString(taskRequestStates) {
+ let filterQueries = {};
+ let sortQueries = {};
+
+ if (taskRequestStates.status) {
+ filterQueries.status = taskRequestStates.status;
+ }
+ if (taskRequestStates.requestType) {
+ filterQueries['request-type'] = taskRequestStates.requestType;
+ }
+ if (taskRequestStates.order) {
+ sortQueries = Order[taskRequestStates.order];
+ }
+
+ const queryString = generateRqlQuery(filterQueries, sortQueries);
+
+ const urlParams = new URLSearchParams();
+ if (taskRequestStates.size) {
+ urlParams.append('size', taskRequestStates.size);
+ }
+ if (queryString) {
+ urlParams.append('q', queryString);
+ }
+ if (taskRequestStates.dev) {
+ urlParams.append('dev', true);
+ }
+ return '?' + urlParams.toString();
+}
+
+const addSpinner = (container) => {
+ const spinner = createCustomElement({
+ tagName: 'div',
+ className: 'spinner',
+ });
+
+ container.append(spinner);
+
+ function removeSpinner() {
+ spinner.remove();
+ }
+
+ return removeSpinner;
+};