diff --git a/__tests__/applications/applications.test.js b/__tests__/applications/applications.test.js index a4a42c26..c1d33687 100644 --- a/__tests__/applications/applications.test.js +++ b/__tests__/applications/applications.test.js @@ -30,16 +30,16 @@ describe('Applications page', () => { page.on('request', (request) => { if ( - request.url() === `${API_BASE_URL}/applications?size=5` || + request.url() === `${API_BASE_URL}/applications?size=6` || request.url() === - `${API_BASE_URL}/applications?next=YwTi6zFNI3GlDsZVjD8C&size=5` + `${API_BASE_URL}/applications?next=YwTi6zFNI3GlDsZVjD8C&size=6` ) { request.respond({ status: 200, contentType: 'application/json', body: JSON.stringify({ applications: fetchedApplications, - next: '/applications?next=YwTi6zFNI3GlDsZVjD8C&size=5', + next: '/applications?next=YwTi6zFNI3GlDsZVjD8C&size=6', }), headers: { 'Access-Control-Allow-Origin': '*', @@ -48,7 +48,7 @@ describe('Applications page', () => { }, }); } else if ( - request.url() === `${API_BASE_URL}/applications?size=5&status=accepted` + request.url() === `${API_BASE_URL}/applications?size=6&status=accepted` ) { request.respond({ status: 200, @@ -72,7 +72,7 @@ describe('Applications page', () => { body: JSON.stringify(superUserForAudiLogs), }); } else if ( - request.url() === `${API_BASE_URL}/applications/lavEduxsb2C5Bl4s289P` + request.url() === `${API_BASE_URL}/applications/lavEduxsb2C6Bl4s289P` ) { request.respond({ status: 200, @@ -109,7 +109,7 @@ describe('Applications page', () => { expect(title).toBeTruthy(); expect(filterButton).toBeTruthy(); expect(applicationCards).toBeTruthy(); - expect(applicationCards.length).toBe(5); + expect(applicationCards.length).toBe(6); }); it('should load and render the accepted application requests when accept is selected from filter, and after clearing the filter it should again show all the applications', async function () { @@ -128,12 +128,16 @@ describe('Applications page', () => { await page.waitForNetworkIdle(); applicationCards = await page.$$('.application-card'); - expect(applicationCards.length).toBe(5); + expect(applicationCards.length).toBe(6); + const urlAfterClearingStatusFilter = new URL(page.url()); + expect( + urlAfterClearingStatusFilter.searchParams.get('status') === null, + ).toBe(true, 'status query param is not removed from url'); }); it('should load more applications on going to the bottom of the page', async function () { let applicationCards = await page.$$('.application-card'); - expect(applicationCards.length).toBe(5); + expect(applicationCards.length).toBe(6); await page.evaluate(() => { const element = document.querySelector('#page_bottom_element'); if (element) { @@ -142,7 +146,7 @@ describe('Applications page', () => { }); await page.waitForNetworkIdle(); applicationCards = await page.$$('.application-card'); - expect(applicationCards.length).toBe(10); + expect(applicationCards.length).toBe(12); }); it('should open application details modal for application, when user click on view details on any card', async function () { @@ -158,9 +162,42 @@ describe('Applications page', () => { el.classList.contains('hidden'), ), ).toBe(false); + const urlAfterOpeningModal = new URL(page.url()); + expect(urlAfterOpeningModal.searchParams.get('id') !== null).toBe(true); + }); + + it('should close application details modal, when user clicks the close button', async function () { + const applicationDetailsModal = await page.$('.application-details'); + await page.click('.view-details-button'); + await applicationDetailsModal.$eval('.application-close-button', (node) => + node.click(), + ); + expect( + await applicationDetailsModal.evaluate((el) => + el.classList.contains('hidden'), + ), + ).toBe(true); + const urlAfterClosingModal = new URL(page.url()); + expect(urlAfterClosingModal.searchParams.get('id') === null).toBe( + true, + 'id query param is not removed from url', + ); + }); + + it('should load all applications behind the modal on applications/?id= page load', async function () { + await page.click('.view-details-button'); + await page.reload(); + await page.waitForNetworkIdle(); + const applicationDetailsModal = await page.$('.application-details'); + await applicationDetailsModal.$eval('.application-close-button', (node) => + node.click(), + ); + const applicationCards = await page.$$('.application-card'); + expect(applicationCards).toBeTruthy(); + expect(applicationCards.length).toBe(6); }); - it('should show toast message with application updated successfully', async function () { + it.skip('should show toast message with application updated successfully', async function () { await page.click('.view-details-button'); await page.click('.application-details-accept'); const toast = await page.$('#toast'); diff --git a/applications/script.js b/applications/script.js index 2b2238df..bee5c39b 100644 --- a/applications/script.js +++ b/applications/script.js @@ -32,6 +32,10 @@ const applyFilterButton = document.getElementById('apply-filter-button'); const applicationContainer = document.querySelector('.application-container'); const clearButton = document.getElementById('clear-button'); const lastElementContainer = document.getElementById('page_bottom_element'); + +const urlParams = new URLSearchParams(window.location.search); +let applicationId = urlParams.get('id'); + let currentApplicationId; let status = 'all'; @@ -46,18 +50,22 @@ function updateUserApplication({ isAccepted }) { payload['status'] = status; - if (applicationTextarea.value) payload.feedback = applicationTextarea.value; + if (applicationTextarea.value) { + payload.feedback = applicationTextarea.value; + } updateApplication({ applicationId: currentApplicationId, applicationPayload: payload, }) .then((res) => { - closeApplicationDetails(); + const updatedFeedback = payload.feedback || ''; + applicationTextarea.value = updatedFeedback; + showToast({ type: 'success', message: res.message }); + setTimeout(() => closeApplicationDetails(), 1000); }) .catch((error) => { - closeApplicationDetails(); showToast({ type: 'error', message: error.message }); }); } @@ -73,6 +81,7 @@ function closeApplicationDetails() { applicationDetailsModal.classList.add('hidden'); backDropBlur.style.display = 'none'; document.body.style.overflow = 'auto'; + removeQueryParamInUrl('id'); } function openApplicationDetails(application) { @@ -170,15 +179,35 @@ function openApplicationDetails(application) { class: 'application-textarea', placeHolder: 'Add Feedback here', }, + innerText: application.feedback || '', }); applicationSection.appendChild(applicationSectionTitle); applicationSection.appendChild(applicationTextArea); applicationDetailsMain.appendChild(applicationSection); + + if (application.status === 'rejected') { + applicationRejectButton.disabled = true; + applicationRejectButton.style.cursor = 'not-allowed'; + applicationRejectButton.classList.add('disable-button'); + } else if (application.status === 'accepted') { + applicationAcceptButton.disabled = true; + applicationAcceptButton.style.cursor = 'not-allowed'; + applicationAcceptButton.classList.add('disable-button'); + } else { + applicationRejectButton.disabled = false; + applicationRejectButton.style.cursor = 'pointer'; + applicationRejectButton.classList.remove('disable-button'); + + applicationAcceptButton.disabled = false; + applicationAcceptButton.style.cursor = 'pointer'; + applicationAcceptButton.classList.remove('disable-button'); + } } function clearFilter() { if (status === 'all') return; + removeQueryParamInUrl('status'); changeFilter(); const selectedFilterOption = document.querySelector( 'input[name="status"]:checked', @@ -193,6 +222,23 @@ function changeLoaderVisibility({ hide }) { else loader.classList.remove('hidden'); } +function addQueryParamInUrl(queryParamKey, queryParamVal) { + const currentUrlParams = new URLSearchParams(window.location.search); + currentUrlParams.append(queryParamKey, queryParamVal); + const updatedUrl = '/applications/?' + currentUrlParams.toString(); + window.history.replaceState(window.history.state, '', updatedUrl); +} + +function removeQueryParamInUrl(queryParamKey) { + const currentUrlParams = new URLSearchParams(window.location.search); + currentUrlParams.delete(queryParamKey); + let updatedUrl = '/applications/'; + if (currentUrlParams.size > 0) { + updatedUrl += '?' + currentUrlParams.toString(); + } + window.history.replaceState(window.history.state, '', updatedUrl); +} + function createApplicationCard({ application }) { const applicationCard = createElement({ type: 'div', @@ -212,13 +258,13 @@ function createApplicationCard({ application }) { const companyNameText = createElement({ type: 'p', - attributes: { class: 'company-name' }, + attributes: { class: 'company-name hide-overflow' }, innerText: `Company name: ${application.professional.institution}`, }); const skillsText = createElement({ type: 'p', - attributes: { class: 'skills' }, + attributes: { class: 'skills hide-overflow' }, innerText: `Skills: ${application.professional.skills}`, }); @@ -228,7 +274,7 @@ function createApplicationCard({ application }) { const introductionText = createElement({ type: 'p', - attributes: { class: 'user-intro' }, + attributes: { class: 'user-intro hide-overflow' }, innerText: application.intro.introduction.slice(0, 200), }); @@ -238,9 +284,10 @@ function createApplicationCard({ application }) { innerText: 'View Details', }); - viewDetailsButton.addEventListener('click', () => - openApplicationDetails(application), - ); + viewDetailsButton.addEventListener('click', () => { + addQueryParamInUrl('id', application.id); + openApplicationDetails(application); + }); applicationCard.appendChild(userInfoContainer); applicationCard.appendChild(introductionText); @@ -283,10 +330,7 @@ async function renderApplicationById(id) { if (!application) { return noApplicationFoundText.classList.remove('hidden'); } - - const applicationCard = createApplicationCard({ application }); - applicationContainer.appendChild(applicationCard); - applicationContainer.classList.add('center'); + openApplicationDetails(application); } catch (error) { console.error('Error fetching application by user ID:', error); noApplicationFoundText.classList.remove('hidden'); @@ -310,17 +354,18 @@ async function renderApplicationById(id) { changeLoaderVisibility({ hide: true }); return; } + const urlParams = new URLSearchParams(window.location.search); + status = urlParams.get('status') || 'all'; - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - const applicationId = urlParams.get('id'); + if (status !== 'all') { + document.querySelector(`input[name="status"]#${status}`).checked = true; + } if (applicationId) { await renderApplicationById(applicationId); - } else { - await renderApplicationCards('', status, true); - addIntersectionObserver(); } + await renderApplicationCards('', status, true, applicationId); + addIntersectionObserver(); changeLoaderVisibility({ hide: true }); })(); @@ -355,8 +400,11 @@ applyFilterButton.addEventListener('click', () => { const selectedFilterOption = document.querySelector( 'input[name="status"]:checked', ); + + const selectedStatus = selectedFilterOption.value; + addQueryParamInUrl('status', selectedStatus); changeFilter(); - status = selectedFilterOption.value; + status = selectedStatus; renderApplicationCards(nextLink, status); }); diff --git a/applications/style.css b/applications/style.css index d620744d..dbd4e518 100644 --- a/applications/style.css +++ b/applications/style.css @@ -147,6 +147,7 @@ body { flex-wrap: wrap; justify-content: space-between; padding-bottom: 10px; + padding-top: 32px; gap: 25px; } @@ -157,7 +158,7 @@ body { .application-card { border-radius: 15px; box-shadow: var(--elevation-1); - padding: 15px; + padding: 24px; width: 44%; display: flex; flex-direction: column; @@ -186,6 +187,7 @@ body { font-weight: 700; line-height: normal; + padding-bottom: 8px; } .application-card .user-info .company-name { @@ -202,6 +204,12 @@ body { line-height: normal; } +.hide-overflow { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .application-card .user-intro { color: var(--color-gray); font-size: 16px; @@ -234,6 +242,10 @@ body { margin: 30px; } +.loader.hidden { + display: none; +} + #page_bottom_element { width: 100%; height: 20px; @@ -269,7 +281,7 @@ body { .application-details .application-details-main { height: 90%; overflow-y: auto; - padding: 15px; + padding: 15px 30px; display: flex; flex-direction: column; gap: 25px; @@ -368,6 +380,10 @@ body { font-weight: 600; } +.no_applications_found.hidden { + display: none; +} + .close-button-icon { width: 32px; height: 32px; @@ -395,6 +411,10 @@ body { background: var(--color-red-variant1); } +.disable-button { + opacity: 0.2; +} + @keyframes slideIn { from { right: -300px; diff --git a/applications/utils.js b/applications/utils.js index cee69fa0..aff58148 100644 --- a/applications/utils.js +++ b/applications/utils.js @@ -13,7 +13,7 @@ function createElement({ type, attributes = {}, innerText }) { return element; } -async function getApplications({ applicationStatus, size = 5, next = '' }) { +async function getApplications({ applicationStatus, size = 6, next = '' }) { let url; if (next) url = `${BASE_URL}${next}`; diff --git a/index.html b/index.html index 6c3856f6..aea4c8e4 100644 --- a/index.html +++ b/index.html @@ -136,11 +136,7 @@ > Activity Feed - + Applications