From 6c3f035794be76b55d58f4e139fb7004b5c196a2 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Mon, 19 Jan 2026 06:52:38 +0530 Subject: [PATCH 1/4] feat: add application detail page --- app/components/application/detail-header.hbs | 81 ++++++++++++ app/components/application/detail-header.js | 117 ++++++++++++++++++ app/components/application/feedback-card.hbs | 13 ++ app/components/application/feedback-card.js | 11 ++ app/components/application/info-card.hbs | 24 ++++ .../application/social-link-pill.hbs | 11 ++ .../application/social-link-pill.js | 16 +++ app/components/application/status-badge.hbs | 3 + app/components/application/status-badge.js | 11 ++ app/components/header.hbs | 2 +- app/components/scroll-to-top.hbs | 8 +- app/constants/applications.js | 39 ++++++ app/controllers/applications/detail.js | 44 +++++++ app/routes/applications/detail.js | 17 ++- app/templates/applications/detail.hbs | 63 +++++++--- tests/unit/routes/applications/detail-test.js | 2 +- 16 files changed, 433 insertions(+), 29 deletions(-) create mode 100644 app/components/application/detail-header.hbs create mode 100644 app/components/application/detail-header.js create mode 100644 app/components/application/feedback-card.hbs create mode 100644 app/components/application/feedback-card.js create mode 100644 app/components/application/info-card.hbs create mode 100644 app/components/application/social-link-pill.hbs create mode 100644 app/components/application/social-link-pill.js create mode 100644 app/components/application/status-badge.hbs create mode 100644 app/components/application/status-badge.js create mode 100644 app/constants/applications.js create mode 100644 app/controllers/applications/detail.js diff --git a/app/components/application/detail-header.hbs b/app/components/application/detail-header.hbs new file mode 100644 index 000000000..ae72dd62d --- /dev/null +++ b/app/components/application/detail-header.hbs @@ -0,0 +1,81 @@ +
+
+
+ Profile +
+
+

+ {{this.fullName}} +

+ +
+

{{this.role}}

+
+
+ + {{this.location}} +
+
+ + {{this.skills}} +
+
+
+
+ + +
+ +
+
+
+ {{this.score}} + Score +
+
+ {{this.nudgeCount}} + Nudges +
+
+
+ {{#if @isAdmin}} + + {{else}} + + + {{/if}} +
+
+
\ No newline at end of file diff --git a/app/components/application/detail-header.js b/app/components/application/detail-header.js new file mode 100644 index 000000000..6830cef68 --- /dev/null +++ b/app/components/application/detail-header.js @@ -0,0 +1,117 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; + +export default class DetailHeader extends Component { + get application() { + return this.args.application; + } + + get userDetails() { + return this.args.userDetails; + } + + get fullName() { + const { firstName, lastName } = this.application?.biodata || {}; + return firstName || lastName ? `${firstName} ${lastName}` : null; + } + + get imageUrl() { + return this.application?.imageUrl ?? ''; + } + + get role() { + return this.application?.role ?? 'N/A'; + } + + get status() { + return this.application?.status ?? 'pending'; + } + + get skills() { + return this.application?.professional?.skills ?? 'N/A'; + } + + get location() { + const { city, state, country } = this.application?.location || {}; + return [city, state, country].filter(Boolean).join(', ') || 'N/A'; + } + + get score() { + return this.application?.score ?? 'N/A'; + } + + get nudgeCount() { + return this.application?.nudgeCount ?? 0; + } + + get lastNudgedAt() { + return this.application?.lastNudgedAt ?? null; + } + + get lastNudgedRelative() { + if (!this.lastNudgedAt) return 'Never'; + + const now = new Date(); + const nudged = new Date(this.lastNudgedAt); + const diffMs = now - nudged; + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffHours / 24); + + if (diffHours < 1) return '<1h ago'; + if (diffHours < 48) return `${diffHours}h ago`; + return `${diffDays} days ago`; + } + + get isNudgeDisabled() { + if (this.status !== 'pending') { + return true; + } + if (!this.lastNudgedAt) { + return false; + } + const now = Date.now(); + const lastNudgeTime = new Date(this.lastNudgedAt).getTime(); + const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; + return now - lastNudgeTime < TWENTY_FOUR_HOURS; + } + + get socialLinks() { + const links = []; + const social = this.application?.socialLink || {}; + + if (social.github) + links.push({ platform: 'GitHub', userName: social.github }); + if (social.linkedin) + links.push({ platform: 'LinkedIn', userName: social.linkedin }); + if (social.twitter) + links.push({ platform: 'Twitter', userName: social.twitter }); + if (social.instagram) + links.push({ platform: 'Instagram', userName: social.instagram }); + if (social.peerlist) + links.push({ platform: 'Peerlist', userName: social.peerlist }); + if (social.dribbble) + links.push({ platform: 'Dribbble', userName: social.dribbble }); + if (social.behance) + links.push({ platform: 'Behance', userName: social.behance }); + + return links; + } + + //ToDo: Implement logic for callling nudge API here + @action + nudgeApplication() { + return false; + } + + //ToDo: Implement logic for edit application here + @action + editApplication() { + return false; + } + + //ToDo: Navigate to dashboard site for admin actions + @action + navigateToDashboard() { + return false; + } +} diff --git a/app/components/application/feedback-card.hbs b/app/components/application/feedback-card.hbs new file mode 100644 index 000000000..b36664b3c --- /dev/null +++ b/app/components/application/feedback-card.hbs @@ -0,0 +1,13 @@ +
+ + +
\ No newline at end of file diff --git a/app/components/application/feedback-card.js b/app/components/application/feedback-card.js new file mode 100644 index 000000000..c8fcc2436 --- /dev/null +++ b/app/components/application/feedback-card.js @@ -0,0 +1,11 @@ +import Component from '@glimmer/component'; + +export default class FeedbackCard extends Component { + get formattedDate() { + if (!this.args.createdAt) { + return 'N/A'; + } + const date = new Date(this.args.createdAt); + return isNaN(date.getTime()) ? 'N/A' : date.toLocaleDateString(); + } +} diff --git a/app/components/application/info-card.hbs b/app/components/application/info-card.hbs new file mode 100644 index 000000000..96b67df9e --- /dev/null +++ b/app/components/application/info-card.hbs @@ -0,0 +1,24 @@ +
+

{{@title}}

+
+ {{#each @sections as |section|}} + {{#if (eq section.type "grid")}} +
+ {{#each section.items as |item|}} +
+ +

{{item.value}}

+
+ {{/each}} +
+ {{else}} +
+
+ +

{{section.value}}

+
+
+ {{/if}} + {{/each}} +
+
\ No newline at end of file diff --git a/app/components/application/social-link-pill.hbs b/app/components/application/social-link-pill.hbs new file mode 100644 index 000000000..bdf35fec0 --- /dev/null +++ b/app/components/application/social-link-pill.hbs @@ -0,0 +1,11 @@ + + + {{@platform}} + \ No newline at end of file diff --git a/app/components/application/social-link-pill.js b/app/components/application/social-link-pill.js new file mode 100644 index 000000000..cb50b89ee --- /dev/null +++ b/app/components/application/social-link-pill.js @@ -0,0 +1,16 @@ +import Component from '@glimmer/component'; +import { mapSocialIcons, mapSocialUrls } from '../../constants/applications'; + +export default class SocialLinkPill extends Component { + get platform() { + return this.args.platform?.toLowerCase(); + } + + get icon() { + return mapSocialIcons[this.platform] || 'mdi:link'; + } + + get redirectUrl() { + return `${mapSocialUrls[this.platform]}/${this.args.userName}`; + } +} diff --git a/app/components/application/status-badge.hbs b/app/components/application/status-badge.hbs new file mode 100644 index 000000000..b5a1da5a7 --- /dev/null +++ b/app/components/application/status-badge.hbs @@ -0,0 +1,3 @@ + + {{this.applicationStatus}} + \ No newline at end of file diff --git a/app/components/application/status-badge.js b/app/components/application/status-badge.js new file mode 100644 index 000000000..9ef5d7116 --- /dev/null +++ b/app/components/application/status-badge.js @@ -0,0 +1,11 @@ +import Component from '@glimmer/component'; +import { mapApplicationStatus } from '../../constants/applications'; + +export default class StatusBadge extends Component { + get applicationStatus() { + const status = this.args.status?.toUpperCase(); + return ( + mapApplicationStatus[status] || mapApplicationStatus.PENDING + )?.toUpperCase(); + } +} diff --git a/app/components/header.hbs b/app/components/header.hbs index f0d22f178..fcb3e82ed 100644 --- a/app/components/header.hbs +++ b/app/components/header.hbs @@ -15,7 +15,7 @@ Real_Dev_Squad diff --git a/app/components/scroll-to-top.hbs b/app/components/scroll-to-top.hbs index 1d98298c2..74c406b6c 100644 --- a/app/components/scroll-to-top.hbs +++ b/app/components/scroll-to-top.hbs @@ -1,7 +1,7 @@ {{#unless this.isLive}} -{{#if this.isScrollToTopVisible}} - + {{#if this.isScrollToTopVisible}} + {{/if}} {{/unless}} \ No newline at end of file diff --git a/app/constants/applications.js b/app/constants/applications.js new file mode 100644 index 000000000..1d23432fe --- /dev/null +++ b/app/constants/applications.js @@ -0,0 +1,39 @@ +export const mapApplicationStatus = { + ACCEPTED: 'accepted', + REJECTED: 'rejected', + CHANGES_REQUESTED: 'Changes Requested', + PENDING: 'pending', +}; + +export const mapSocialIcons = { + github: 'mdi:github', + linkedin: 'mdi:linkedin', + twitter: 'mdi:twitter', + instagram: 'mdi:instagram', + peerlist: 'mdi:account-circle', +}; + +export const mapSocialUrls = { + github: 'https://github.com', + linkedin: 'https://linkedin.com', + twitter: 'https://twitter.com', + instagram: 'https://instagram.com', + peerlist: 'https://peerlist.io', + dribbble: 'https://dribbble.com', + behance: 'https://behance.net', +}; + +export function adminMessage(status) { + switch (status) { + case 'pending': + return 'Admins are reviewing your applications, please hold up and keep on nudging.'; + case 'accepted': + return 'Your application is approved, go back to join page to get join discord invite.'; + case 'rejected': + return 'Your application has been rejected. Look at the feedback for more information.'; + case 'changes_requested': + return 'Admin has requested changes on your application, please edit and submit again for review.'; + default: + return 'Unknown status'; + } +} diff --git a/app/controllers/applications/detail.js b/app/controllers/applications/detail.js new file mode 100644 index 000000000..9581f34bb --- /dev/null +++ b/app/controllers/applications/detail.js @@ -0,0 +1,44 @@ +import Controller from '@ember/controller'; +import { adminMessage } from '../../constants/applications'; + +export default class ApplicationsDetailController extends Controller { + get application() { + return this.model?.application; + } + + get currentUser() { + return this.model?.currentUser; + } + + get isAdmin() { + return this.currentUser?.roles?.super_user === true; + } + + get isApplicant() { + return this.currentUser?.id === this.application?.userId; + } + + get canAccessApplication() { + return this.isAdmin || this.isApplicant; + } + + get aboutYouSections() { + return [ + { + label: 'Introduction', + value: this.application?.intro?.introduction || 'N/A', + }, + { label: 'Fun Fact', value: this.application?.intro?.funFact || 'N/A' }, + { label: 'For Fun', value: this.application?.intro?.forFun || 'N/A' }, + { label: 'Why Join Us', value: this.application?.intro?.whyRds || 'N/A' }, + ]; + } + + get hasFeedback() { + return this.application?.feedback?.length > 0; + } + + get showAdminMessage() { + return adminMessage(this.application?.status); + } +} diff --git a/app/routes/applications/detail.js b/app/routes/applications/detail.js index 7df24e838..eac84ecee 100644 --- a/app/routes/applications/detail.js +++ b/app/routes/applications/detail.js @@ -16,15 +16,10 @@ export default class ApplicationsDetailRoute extends Route { async model(params) { try { const userResponse = await apiRequest(SELF_USER_PROFILE_URL); - if (userResponse.status === 401) { this.toast.error(ERROR_MESSAGES.notLoggedIn, '', TOAST_OPTIONS); setTimeout(redirectAuth, 2000); - return null; - } - - if (!userResponse.ok) { - throw new Error(`HTTP error! status: ${userResponse.status}`); + return { application: null, currentUser: null }; } const applicationResponse = await apiRequest( @@ -33,22 +28,26 @@ export default class ApplicationsDetailRoute extends Route { if (applicationResponse.status === 404) { this.toast.error('Application not found', 'Error!', TOAST_OPTIONS); - return null; + return { application: null, currentUser: null }; } if (!applicationResponse.ok) { throw new Error(`HTTP error! status: ${applicationResponse.status}`); } + const userData = await userResponse.json(); const applicationData = await applicationResponse.json(); - return applicationData?.application; + return { + application: applicationData?.application, + currentUser: userData, + }; } catch (error) { this.toast.error( 'Something went wrong. ' + error.message, 'Error!', TOAST_OPTIONS, ); - return null; + return { application: null, currentUser: null }; } } } diff --git a/app/templates/applications/detail.hbs b/app/templates/applications/detail.hbs index 75a1f290f..a45af36dd 100644 --- a/app/templates/applications/detail.hbs +++ b/app/templates/applications/detail.hbs @@ -1,18 +1,53 @@ -
-

Application Details

- {{#if this.model}} -
-
-

Basic Information

-

Application ID: {{this.model.id}}

-

User ID: {{this.model.userId}}

+
+ {{#if this.application}} + + +
+ + {{this.showAdminMessage}}
-
+ +
+
+ +
+ +
+

Feedback History

+ +
+
+ {{else}} -
-

Application not found.

-

The application you're looking for doesn't - exist or may have been removed.

-
+
+

Application not found.

+

The application you're looking for doesn't + exist or may have been removed.

+
{{/if}}
\ No newline at end of file diff --git a/tests/unit/routes/applications/detail-test.js b/tests/unit/routes/applications/detail-test.js index 6175738cb..3587c2fec 100644 --- a/tests/unit/routes/applications/detail-test.js +++ b/tests/unit/routes/applications/detail-test.js @@ -6,7 +6,7 @@ import { SELF_USER_PROFILE_URL, } from 'website-www/constants/apis'; -module('Unit | Route | applications/detail', function (hooks) { +module.skip('Unit | Route | applications/detail', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { From 2aa61dd01801e013064271c5c3df20879655fc3e Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Mon, 19 Jan 2026 19:34:14 +0530 Subject: [PATCH 2/4] refactor: remove unused getters, simplify info card section --- app/components/application/detail-header.hbs | 6 ++-- app/components/application/detail-header.js | 30 ++++---------------- app/components/application/feedback-card.hbs | 13 +++++---- app/components/application/info-card.hbs | 21 +++----------- app/templates/applications/detail.hbs | 4 +-- 5 files changed, 22 insertions(+), 52 deletions(-) diff --git a/app/components/application/detail-header.hbs b/app/components/application/detail-header.hbs index ae72dd62d..34849825e 100644 --- a/app/components/application/detail-header.hbs +++ b/app/components/application/detail-header.hbs @@ -57,7 +57,7 @@ @variant="dark" @class="btn--xs" @onClick={{this.navigateToDashboard}} - data-test-navigate-button + @test="navigate-button" /> {{else}} {{/if}}
diff --git a/app/components/application/detail-header.js b/app/components/application/detail-header.js index 6830cef68..ce7a8f79a 100644 --- a/app/components/application/detail-header.js +++ b/app/components/application/detail-header.js @@ -44,24 +44,6 @@ export default class DetailHeader extends Component { return this.application?.nudgeCount ?? 0; } - get lastNudgedAt() { - return this.application?.lastNudgedAt ?? null; - } - - get lastNudgedRelative() { - if (!this.lastNudgedAt) return 'Never'; - - const now = new Date(); - const nudged = new Date(this.lastNudgedAt); - const diffMs = now - nudged; - const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); - const diffDays = Math.floor(diffHours / 24); - - if (diffHours < 1) return '<1h ago'; - if (diffHours < 48) return `${diffHours}h ago`; - return `${diffDays} days ago`; - } - get isNudgeDisabled() { if (this.status !== 'pending') { return true; @@ -97,21 +79,21 @@ export default class DetailHeader extends Component { return links; } - //ToDo: Implement logic for callling nudge API here @action nudgeApplication() { - return false; + //ToDo: Implement logic for callling nudge API here + console.log('nudge application'); } - //ToDo: Implement logic for edit application here @action editApplication() { - return false; + //ToDo: Implement logic for edit application here + console.log('edit application'); } - //ToDo: Navigate to dashboard site for admin actions @action navigateToDashboard() { - return false; + //ToDo: Navigate to dashboard site for admin actions + console.log('navigate to dashboard'); } } diff --git a/app/components/application/feedback-card.hbs b/app/components/application/feedback-card.hbs index b36664b3c..9422bd372 100644 --- a/app/components/application/feedback-card.hbs +++ b/app/components/application/feedback-card.hbs @@ -1,13 +1,14 @@ \ No newline at end of file diff --git a/app/components/application/info-card.hbs b/app/components/application/info-card.hbs index 96b67df9e..13f544e9b 100644 --- a/app/components/application/info-card.hbs +++ b/app/components/application/info-card.hbs @@ -2,23 +2,10 @@

{{@title}}

{{#each @sections as |section|}} - {{#if (eq section.type "grid")}} -
- {{#each section.items as |item|}} -
- -

{{item.value}}

-
- {{/each}} -
- {{else}} -
-
- -

{{section.value}}

-
-
- {{/if}} +
+ +

{{section.value}}

+
{{/each}}
\ No newline at end of file diff --git a/app/templates/applications/detail.hbs b/app/templates/applications/detail.hbs index a45af36dd..0a88f7023 100644 --- a/app/templates/applications/detail.hbs +++ b/app/templates/applications/detail.hbs @@ -46,8 +46,8 @@ {{else}}

Application not found.

-

The application you're looking for doesn't - exist or may have been removed.

+

The application + you're looking for doesn't exist or may have been removed.

{{/if}} \ No newline at end of file From 9bd155a96eda5d9e60737b3607a89c63ee218ae8 Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Mon, 19 Jan 2026 20:57:46 +0530 Subject: [PATCH 3/4] fix: incorrect usage of lastNudge and invalid linkedin url --- app/components/application/detail-header.js | 4 ++-- app/constants/applications.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/application/detail-header.js b/app/components/application/detail-header.js index ce7a8f79a..b7df3dde6 100644 --- a/app/components/application/detail-header.js +++ b/app/components/application/detail-header.js @@ -48,11 +48,11 @@ export default class DetailHeader extends Component { if (this.status !== 'pending') { return true; } - if (!this.lastNudgedAt) { + if (!this.application?.lastNudgedAt) { return false; } const now = Date.now(); - const lastNudgeTime = new Date(this.lastNudgedAt).getTime(); + const lastNudgeTime = new Date(this.application.lastNudgedAt).getTime(); const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000; return now - lastNudgeTime < TWENTY_FOUR_HOURS; } diff --git a/app/constants/applications.js b/app/constants/applications.js index 1d23432fe..c3fda4297 100644 --- a/app/constants/applications.js +++ b/app/constants/applications.js @@ -15,7 +15,7 @@ export const mapSocialIcons = { export const mapSocialUrls = { github: 'https://github.com', - linkedin: 'https://linkedin.com', + linkedin: 'https://linkedin.com/in', twitter: 'https://twitter.com', instagram: 'https://instagram.com', peerlist: 'https://peerlist.io', From 3bea8987db96c4191afdd08c5d9037d56865ad3d Mon Sep 17 00:00:00 2001 From: Mayank Bansal Date: Wed, 21 Jan 2026 00:29:54 +0530 Subject: [PATCH 4/4] test: add unit and integration tests for application detail page (#1137) * test: add unit tests for routes and controllers for detail page * test: add integration tests for application components * fix: add more expectation and cleanup tests * refactor: use constants for applications data --- tests/constants/application-data.js | 65 +++++++++++++ .../application/detail-header-test.js | 96 +++++++++++++++++++ .../application/feedback-card-test.js | 59 ++++++++++++ .../components/application/info-card-test.js | 48 ++++++++++ .../application/social-link-pill-test.js | 51 ++++++++++ .../application/status-badge-test.js | 32 +++++++ .../controllers/applications/detail-test.js | 91 ++++++++++++++++++ tests/unit/routes/applications/detail-test.js | 31 ++++-- 8 files changed, 465 insertions(+), 8 deletions(-) create mode 100644 tests/constants/application-data.js create mode 100644 tests/integration/components/application/detail-header-test.js create mode 100644 tests/integration/components/application/feedback-card-test.js create mode 100644 tests/integration/components/application/info-card-test.js create mode 100644 tests/integration/components/application/social-link-pill-test.js create mode 100644 tests/integration/components/application/status-badge-test.js create mode 100644 tests/unit/controllers/applications/detail-test.js diff --git a/tests/constants/application-data.js b/tests/constants/application-data.js new file mode 100644 index 000000000..435a7108f --- /dev/null +++ b/tests/constants/application-data.js @@ -0,0 +1,65 @@ +export const APPLICATIONS_DATA = { + id: '2MzNJtwGL3D28WeC7yUI', + userId: 'SoaTFu1SnU7HVvWjkR7Z', + biodata: { firstName: 'John', lastName: 'Doe' }, + location: { + city: 'Patna', + state: 'Bihar', + country: 'India', + }, + professional: { + institution: 'MIT', + skills: + 'JavaScript, Node.js, React, Next, Ember.js, Typescript, Java, Springboot, Docker, Azure, AWS, GenAI, AI Agents, Langchain, Redis, Postgresql, MongoDB, WebSockets', + }, + intro: { + introduction: + 'I am a passionate developer with 5 years of experience. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + funFact: + 'I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users.I once built a full-stack application in 48 hours during a hackathon. The project went on to win first place and is now used by over 10,000 users. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + forFun: + 'I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking.I love coding, reading tech blogs, and contributing to open source projects. In my free time, I enjoy playing guitar and hiking. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + whyRds: + 'I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects.I am excited to join Real Dev Squad because of its commitment to open source and community-driven development. I believe in the mission and want to contribute to meaningful projects. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + numberOfHours: 20, + }, + foundFrom: 'LinkedIn', + role: 'developer', + imageUrl: + 'https://res.cloudinary.com/imgprc/image/upload/v1767280230/profile/nEYwqHm8vGoZhh1j9PbF/no8jkcirgj3sb9phfm20.jpg', + socialLink: { + github: 'johndoe', + linkedin: 'johndoe', + twitter: 'Johndoe', + peerlist: 'johndoe', + instagram: 'johndoe', + dribbble: 'johndoe', + behance: 'johndoe', + }, + feedback: [ + { + status: 'accepted', + feedback: '', + reviewerName: 'Ankush', + createdAt: '2026-01-14T15:37:10.991Z', + }, + { + status: 'changes_requested', + feedback: 'Please follow requirements for the profile picture', + reviewerName: 'Prakash', + createdAt: '2026-01-13T12:37:10.991Z', + }, + { + status: 'rejected', + feedback: 'lorem', + reviewerName: 'Tejas', + createdAt: '2026-01-11T10:37:10.991Z', + }, + ], + score: 250, + status: 'accepted', + createdAt: '2026-01-14T18:37:10.991Z', + nudgeCount: 10, + isNew: false, + notFound: false, +}; diff --git a/tests/integration/components/application/detail-header-test.js b/tests/integration/components/application/detail-header-test.js new file mode 100644 index 000000000..e1e3c771d --- /dev/null +++ b/tests/integration/components/application/detail-header-test.js @@ -0,0 +1,96 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { APPLICATIONS_DATA } from 'website-www/tests/constants/application-data'; + +module('Integration | Component | application/detail-header', function (hooks) { + setupRenderingTest(hooks); + + test('it renders application details correctly', async function (assert) { + const user = APPLICATIONS_DATA; + this.set('application', user); + this.set('isAdmin', false); + + await render(hbs` + + `); + + const { firstName, lastName } = user.biodata; + assert + .dom('[data-test-user-name]') + .includesText(`${firstName} ${lastName}`); + assert.dom('[data-test-status-badge]').hasText(user.status.toUpperCase()); + assert.dom('[data-test-score-value]').hasText(String(user.score)); + assert + .dom('[data-test-nudge-details]') + .includesText(String(user.nudgeCount)); + assert + .dom('[data-test-social-link]') + .exists({ count: Object.keys(user.socialLink).length }); + + assert.dom('[data-test-button="nudge-button"]').exists(); + assert.dom('[data-test-button="edit-button"]').exists(); + assert.dom('[data-test-button="navigate-button"]').doesNotExist(); + }); + + test('it renders admin actions correctly', async function (assert) { + const user = APPLICATIONS_DATA; + this.set('application', user); + this.set('isAdmin', true); + + await render(hbs` + + `); + + assert.dom('[data-test-button="navigate-button"]').exists(); + assert.dom('[data-test-button="nudge-button"]').doesNotExist(); + assert.dom('[data-test-button="edit-button"]').doesNotExist(); + }); + + test('it handles partial location data correctly', async function (assert) { + this.set('application', { + location: { city: 'Mumbai', country: 'India' }, + }); + await render( + hbs``, + ); + assert.dom('[data-test-header-profile]').includesText('Mumbai, India'); + }); + + test('it disables nudge button when status is not pending', async function (assert) { + const app = { ...APPLICATIONS_DATA, status: 'accepted' }; + this.set('application', app); + this.set('isAdmin', false); + + await render(hbs` + + `); + + assert.dom('[data-test-button="nudge-button"]').hasAttribute('disabled'); + }); + + test('it disables nudge button based on 24h timeout', async function (assert) { + const now = Date.now(); + const recentNudge = new Date(now - 12 * 60 * 60 * 1000).toISOString(); + + this.set('application', { + status: 'pending', + lastNudgedAt: recentNudge, + }); + + await render( + hbs``, + ); + assert.dom('[data-test-button="nudge-button"]').hasAttribute('disabled'); + }); +}); diff --git a/tests/integration/components/application/feedback-card-test.js b/tests/integration/components/application/feedback-card-test.js new file mode 100644 index 000000000..b3571b459 --- /dev/null +++ b/tests/integration/components/application/feedback-card-test.js @@ -0,0 +1,59 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { APPLICATIONS_DATA } from 'website-www/tests/constants/application-data'; + +module('Integration | Component | application/feedback-card', function (hooks) { + setupRenderingTest(hooks); + + test('it renders feedback details correctly', async function (assert) { + const feedback = APPLICATIONS_DATA.feedback[0]; + this.set('feedback', feedback); + + await render(hbs` + + `); + + assert + .dom('[data-test-status-badge]') + .hasText(feedback.status.toUpperCase()); + assert.dom('[data-test-feedback-text]').hasText(feedback.feedback); + assert + .dom('[data-test-feedback-reviewer]') + .includesText(feedback.reviewerName); + assert + .dom('[data-test-feedback-date]') + .hasText(new Date(feedback.createdAt).toLocaleDateString()); + }); + + test('it renders N/A for missing date', async function (assert) { + await render(hbs` + + `); + + assert.dom('[data-test-feedback-date]').hasText('N/A'); + }); + + test('it handles invalid date string correctly', async function (assert) { + await render(hbs` + + `); + + assert.dom('[data-test-feedback-date]').hasText('N/A'); + }); +}); diff --git a/tests/integration/components/application/info-card-test.js b/tests/integration/components/application/info-card-test.js new file mode 100644 index 000000000..089303dab --- /dev/null +++ b/tests/integration/components/application/info-card-test.js @@ -0,0 +1,48 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | application/info-card', function (hooks) { + setupRenderingTest(hooks); + + test('it renders title and default sections correctly', async function (assert) { + this.set('sections', [ + { label: 'Intro', value: 'Value 1' }, + { label: 'Fact', value: 'Value 2' }, + ]); + + await render( + hbs``, + ); + + assert.dom('[data-test-info-card-title]').hasText('About'); + assert.dom('[data-test-info-section]').exists({ count: 2 }); + assert.dom('.info-label').includesText('Intro'); + assert.dom('.info-value').includesText('Value 1'); + + assert + .dom('[data-test-info-section]:nth-child(1) .info-label') + .hasText('Intro'); + assert + .dom('[data-test-info-section]:nth-child(1) .info-value') + .hasText('Value 1'); + assert + .dom('[data-test-info-section]:nth-child(2) .info-label') + .hasText('Fact'); + assert + .dom('[data-test-info-section]:nth-child(2) .info-value') + .hasText('Value 2'); + }); + + test('it handles empty sections correctly', async function (assert) { + this.set('sections', []); + + await render( + hbs``, + ); + + assert.dom('[data-test-info-card-title]').hasText('Empty'); + assert.dom('[data-test-info-section]').doesNotExist(); + }); +}); diff --git a/tests/integration/components/application/social-link-pill-test.js b/tests/integration/components/application/social-link-pill-test.js new file mode 100644 index 000000000..ea059cbe8 --- /dev/null +++ b/tests/integration/components/application/social-link-pill-test.js @@ -0,0 +1,51 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module( + 'Integration | Component | application/social-link-pill', + function (hooks) { + setupRenderingTest(hooks); + + test('it renders the platform and links correctly', async function (assert) { + this.set('platform', 'GitHub'); + this.set('userName', 'testuser'); + + await render( + hbs``, + ); + + assert + .dom('[data-test-social-link]') + .hasAttribute('href', 'https://github.com/testuser'); + assert.dom('[data-test-social-link]').includesText('GitHub'); + assert.dom('[data-test-platform="GitHub"]').exists(); + }); + + test('it renders LinkedIn and other platforms correctly', async function (assert) { + this.set('platform', 'LinkedIn'); + this.set('userName', 'testuser'); + + await render( + hbs``, + ); + + assert + .dom('[data-test-social-link]') + .hasAttribute('href', 'https://linkedin.com/in/testuser'); + assert.dom('[data-test-social-link]').includesText('LinkedIn'); + }); + + test('it handles unknown platforms with fallback icon', async function (assert) { + this.set('platform', 'unknown'); + this.set('userName', 'user'); + + await render( + hbs``, + ); + assert.dom('[data-test-social-link]').exists(); + assert.dom('[data-test-social-link]').includesText('unknown'); + }); + }, +); diff --git a/tests/integration/components/application/status-badge-test.js b/tests/integration/components/application/status-badge-test.js new file mode 100644 index 000000000..75fb346ca --- /dev/null +++ b/tests/integration/components/application/status-badge-test.js @@ -0,0 +1,32 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'website-www/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | application/status-badge', function (hooks) { + setupRenderingTest(hooks); + + test('it renders the status in uppercase', async function (assert) { + await render(hbs``); + assert.dom('[data-test-status-badge]').hasText('ACCEPTED'); + + await render(hbs``); + assert.dom('[data-test-status-badge]').hasText('PENDING'); + + await render(hbs``); + assert.dom('[data-test-status-badge]').hasText('CHANGES REQUESTED'); + }); + + test('it applies the correct status class', async function (assert) { + await render(hbs``); + assert.dom('[data-test-status-badge]').hasClass('status-badge--accepted'); + + await render(hbs``); + assert.dom('[data-test-status-badge]').hasClass('status-badge--rejected'); + }); + + test('it fallbacks to PENDING for unknown status', async function (assert) { + await render(hbs``); + assert.dom('[data-test-status-badge]').hasText('PENDING'); + }); +}); diff --git a/tests/unit/controllers/applications/detail-test.js b/tests/unit/controllers/applications/detail-test.js new file mode 100644 index 000000000..de6007136 --- /dev/null +++ b/tests/unit/controllers/applications/detail-test.js @@ -0,0 +1,91 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'website-www/tests/helpers'; +import { APPLICATIONS_DATA } from 'website-www/tests/constants/application-data'; + +module('Unit | Controller | applications/detail', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.controller = this.owner.lookup('controller:applications/detail'); + }); + + test('application details controller exists', function (assert) { + assert.ok(this.controller, 'Controller for application detail exists!'); + }); + + test('isAdmin correctly identifies super_user role', function (assert) { + this.controller.model = { currentUser: { roles: { super_user: true } } }; + assert.true(this.controller.isAdmin, 'True when super_user'); + + this.controller.model = { currentUser: { roles: { super_user: false } } }; + assert.false(this.controller.isAdmin, 'False when not super_user'); + }); + + test('isApplicant correctly identifies application owner', function (assert) { + const userId = APPLICATIONS_DATA.userId; + this.controller.model = { + currentUser: { id: userId }, + application: { userId }, + }; + assert.true(this.controller.isApplicant, 'True when IDs match'); + + this.controller.model = { + currentUser: { id: userId }, + application: { userId: 'other' }, + }; + assert.false(this.controller.isApplicant, 'False when IDs mismatch'); + }); + + test('canAccessApplication correctly returns application access', function (assert) { + this.controller.model = { + currentUser: { roles: { super_user: true } }, + application: { userId: 'other' }, + }; + assert.true(this.controller.canAccessApplication, 'Admin access'); + + this.controller.model = { + currentUser: { id: 'user1' }, + application: { userId: 'user1' }, + }; + assert.true(this.controller.canAccessApplication, 'Applicant access'); + + this.controller.model = { + currentUser: { id: 'user1', roles: { super_user: false } }, + application: { userId: 'user2' }, + }; + assert.false(this.controller.canAccessApplication, 'No access'); + }); + + test('aboutYouSections getter formats the data correctly', function (assert) { + this.controller.model = { application: APPLICATIONS_DATA }; + + const expected = [ + { label: 'Introduction', value: APPLICATIONS_DATA.intro.introduction }, + { label: 'Fun Fact', value: APPLICATIONS_DATA.intro.funFact }, + { label: 'For Fun', value: APPLICATIONS_DATA.intro.forFun }, + { label: 'Why Join Us', value: APPLICATIONS_DATA.intro.whyRds }, + ]; + + assert.deepEqual( + this.controller.aboutYouSections, + expected, + 'Formatted correctly', + ); + }); + + test('hasFeedback correctly identifies feedback presence', function (assert) { + this.controller.model = { application: APPLICATIONS_DATA }; + assert.true(this.controller.hasFeedback, 'True when feedback present'); + + this.controller.model = { application: { feedback: [] } }; + assert.false(this.controller.hasFeedback, 'False when empty'); + }); + + test('showAdminMessage correctly maps application status', function (assert) { + this.controller.model = { application: { status: 'pending' } }; + assert.ok(this.controller.showAdminMessage.includes('reviewing')); + + this.controller.model = { application: { status: 'accepted' } }; + assert.ok(this.controller.showAdminMessage.includes('approved')); + }); +}); diff --git a/tests/unit/routes/applications/detail-test.js b/tests/unit/routes/applications/detail-test.js index 3587c2fec..8335e4295 100644 --- a/tests/unit/routes/applications/detail-test.js +++ b/tests/unit/routes/applications/detail-test.js @@ -6,7 +6,7 @@ import { SELF_USER_PROFILE_URL, } from 'website-www/constants/apis'; -module.skip('Unit | Route | applications/detail', function (hooks) { +module('Unit | Route | applications/detail', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { @@ -28,13 +28,12 @@ module.skip('Unit | Route | applications/detail', function (hooks) { test('fetches application by id successfully', async function (assert) { const mockApplication = { id: '123', userId: 'user1' }; + const mockUser = { first_name: 'John' }; const applicationId = '123'; this.fetchStub .onCall(0) - .resolves( - new Response(JSON.stringify({ first_name: 'John' }), { status: 200 }), - ); + .resolves(new Response(JSON.stringify(mockUser), { status: 200 })); this.fetchStub.onCall(1).resolves( new Response(JSON.stringify({ application: mockApplication }), { @@ -44,7 +43,11 @@ module.skip('Unit | Route | applications/detail', function (hooks) { const result = await this.route.model({ id: applicationId }); - assert.deepEqual(result, mockApplication, 'Returns application from API'); + assert.deepEqual( + result, + { application: mockApplication, currentUser: mockUser }, + 'Returns application and currentUser from API', + ); assert.ok( this.fetchStub.firstCall.calledWith( SELF_USER_PROFILE_URL, @@ -66,7 +69,11 @@ module.skip('Unit | Route | applications/detail', function (hooks) { const result = await this.route.model({ id: '123' }); - assert.strictEqual(result, null, 'Returns null for 401'); + assert.deepEqual( + result, + { application: null, currentUser: null }, + 'Returns null object for 401', + ); assert.ok(this.route.toast.error.calledOnce, 'Error toast is displayed'); }); @@ -80,7 +87,11 @@ module.skip('Unit | Route | applications/detail', function (hooks) { const result = await this.route.model({ id: '123' }); - assert.strictEqual(result, null, 'Returns null for 404'); + assert.deepEqual( + result, + { application: null, currentUser: null }, + 'Returns null object for 404', + ); assert.ok( this.route.toast.error.calledOnce, 'Error toast is displayed for 404', @@ -97,7 +108,11 @@ module.skip('Unit | Route | applications/detail', function (hooks) { const result = await this.route.model({ id: '123' }); - assert.strictEqual(result, null, 'Returns null on error'); + assert.deepEqual( + result, + { application: null, currentUser: null }, + 'Returns null object on error', + ); assert.ok(this.route.toast.error.calledOnce, 'Error toast is displayed'); }); });