diff --git a/app/constants/application.js b/app/constants/application.js new file mode 100644 index 000000000..68b23897a --- /dev/null +++ b/app/constants/application.js @@ -0,0 +1,5 @@ +export const APPLICATION_STATUS_TYPES = { + accepted: 'accepted', + rejected: 'rejected', + pending: 'pending', +}; diff --git a/app/controllers/intro.js b/app/controllers/intro.js index 97daee973..d1af9472c 100644 --- a/app/controllers/intro.js +++ b/app/controllers/intro.js @@ -1,6 +1,153 @@ import Controller from '@ember/controller'; +import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { APPS } from '../constants/urls'; +import { validateApplicationDetails } from '../utils/validate-application-details'; +import { APPLICATION_STATUS_TYPES } from '../constants/application'; +import { TOAST_OPTIONS } from '../constants/toast-options'; export default class IntroController extends Controller { @service login; + @service toast; + @service onboarding; + + @tracked remarks = ''; + @tracked inviteLink = ''; + @tracked isRejected = false; + @tracked isAccepted = false; + @tracked isPending = false; + @tracked feedback = ''; + + statusTypes = APPLICATION_STATUS_TYPES; + + constructor() { + super(...arguments); + } + + get applicationDetails() { + return this.model?.data?.[0]; + } + + initializeStatusFlags() { + if (this.applicationDetails) { + const details = this.applicationDetails; + this.isAccepted = details.status === this.statusTypes.accepted; + this.isRejected = details.status === this.statusTypes.rejected; + this.isPending = details.status === this.statusTypes.pending; + + this.feedback = details.feedback || ''; + if (this.isAccepted) { + this.inviteLink = details.inviteLink || ''; + } + } else { + this.isAccepted = false; + this.isRejected = false; + this.isPending = false; + } + } + + @action + updateRemarks(e) { + this.remarks = e.target.value; + } + + @action + async approveRejectAction(status) { + const initialApplicationDetails = this.model; + + const { isValid, data } = validateApplicationDetails( + initialApplicationDetails, + ); + + if (!isValid || !data) { + console.error('Invalid application details:', initialApplicationDetails); + this.toast.error('Invalid application details.', 'Error!', TOAST_OPTIONS); + return; + } + + const applicationId = data.id; + const body = { status, feedback: this.remarks }; + try { + const response = await fetch( + `${APPS.API_BACKEND}/applications/${applicationId}`, + { + method: 'PATCH', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }, + ); + + if (!response.ok) { + console.error( + 'Failed to update application status. Response:', + response, + ); + this.toast.error( + 'Failed to update application status.', + 'Error!', + TOAST_OPTIONS, + ); + return; + } + + if (status === this.statusTypes.accepted) { + this.inviteLink = await this.onboarding.discordInvite(); + this.isAccepted = true; + this.isRejected = false; + this.isPending = false; + } else if (status === this.statusTypes.rejected) { + this.isRejected = true; + this.isAccepted = false; + this.isPending = false; + } else if (status === this.statusTypes.pending) { + this.isPending = true; + this.isRejected = false; + this.isAccepted = false; + } + + console.log('Updated status flags:', { + isAccepted: this.isAccepted, + isRejected: this.isRejected, + isPending: this.isPending, + }); + + this.toast.success( + `Application ${status.toLowerCase()} successfully.`, + 'Success!', + TOAST_OPTIONS, + ); + } catch (error) { + console.error('Error updating application status:', error); + this.toast.error( + 'Something went wrong, please try again later.', + 'Error!', + TOAST_OPTIONS, + ); + } + } + + @action + copyToClipboard(link) { + navigator.clipboard + .writeText(link) + .then(() => { + this.toast.success( + 'Invite link copied to clipboard!', + 'Success!', + TOAST_OPTIONS, + ); + }) + .catch((err) => { + console.error('Failed to copy the text: ', err); + this.toast.error( + 'Failed to copy the link. Please try again.', + 'Error!', + TOAST_OPTIONS, + ); + }); + } } diff --git a/app/routes/intro.js b/app/routes/intro.js index be26d66d9..c18d65b17 100644 --- a/app/routes/intro.js +++ b/app/routes/intro.js @@ -13,15 +13,25 @@ export default class IntroRoute extends Route { async model(params) { const userId = params.id; - const response = await fetch(APPLICATION_URL(userId), { - credentials: 'include', - }); + try { + const response = await fetch(APPLICATION_URL(userId), { + credentials: 'include', + }); + if (response.status === 404) { + this.router.transitionTo('/page-not-found'); + return {}; + } - if (response.status === 404) { - this.router.transitionTo('/page-not-found'); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching application data:', error); + return {}; } + } - const data = await response.json(); - return data.data; + setupController(controller, model) { + super.setupController(controller, model); + controller.initializeStatusFlags(); } } diff --git a/app/styles/app.css b/app/styles/app.css index 9de6edd71..787e888ce 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -41,6 +41,7 @@ @import url("tooltip.module.css"); @import url("debug.module.css"); @import url("unauthenticated.module.css"); +@import url("feedback.css"); * { margin: 0; diff --git a/app/styles/button.module.css b/app/styles/button.module.css index f3a731174..4766b47aa 100644 --- a/app/styles/button.module.css +++ b/app/styles/button.module.css @@ -53,3 +53,49 @@ transition: all 0.5s ease; width: 14rem; } + +.action-buttons { + margin-bottom: 20px; +} + +.action-buttons button { + font-size: 18px; + padding: 10px 20px; + border-radius: 5px; + border: none; + cursor: pointer; + margin-right: 10px; +} + +.approve-button { + background-color: #28a745; + color: white; +} + +.reject-button { + background-color: #dc3545; + color: white; +} + +.approve-button:hover { + background-color: #218838; +} + +.reject-button:hover { + background-color: #c82333; +} + +.copy-button { + background-color: #007bff; + color: white; + font-size: 14px; + padding: 8px 16px; + border-radius: 5px; + border: none; + cursor: pointer; + margin-left: 10px; +} + +.copy-button:hover { + background-color: #0056b3; +} diff --git a/app/styles/feedback.css b/app/styles/feedback.css new file mode 100644 index 000000000..33cd066f2 --- /dev/null +++ b/app/styles/feedback.css @@ -0,0 +1,18 @@ +.superuser-feedback { + width: 34rem; + padding: 1rem 0; + display: flex; + flex-direction: column; +} + +.superuser-feedback__label { + display: block; + font-size: 1.25rem; + font-weight: 700; + padding-bottom: 0.5rem; +} + +.superuser-feedback__textarea { + padding: 0.5rem; + border-radius: 0.5rem; +} diff --git a/app/templates/intro.hbs b/app/templates/intro.hbs index 1877043af..bce8dcb25 100644 --- a/app/templates/intro.hbs +++ b/app/templates/intro.hbs @@ -1,18 +1,65 @@ -{{page-title "Intro"}} -
{{#if this.login.isLoading}} -
- +
+
- {{else}} - {{#if this.login.userData.roles.super_user}} - +

Congratulations. You've been approved to join our Discord server using this link:

+ {{this.inviteLink}} + +

{{this.feedback}}

+
+ {{else if this.isRejected}} +
+

Unfortunately, at this time, your application is not approved.

+

{{this.feedback}}

+
+ {{else if this.isPending}} +
We are reviewing your application. Please check again here after some time for an update.
+ {{else if this.login.userData.roles.super_user}} + + @isSuperUser={{this.login.userData.roles.super_user}} + /> + +
+ + +
+ +
+ + + + + +
{{else}} - +
You're not authorized to view this page.
{{/if}} + {{else}} +
No application details found.
{{/if}} -
\ No newline at end of file + diff --git a/app/utils/validate-application-details.js b/app/utils/validate-application-details.js new file mode 100644 index 000000000..3a47fa42a --- /dev/null +++ b/app/utils/validate-application-details.js @@ -0,0 +1,36 @@ +/** + * Generate application details + * --- + * @param {Object} applicationDetails + * @param {string} applicationDetails.id + * @param {Object} applicationDetails.intro + * @param {string} applicationDetails.intro.funFact + * @param {string} applicationDetails.intro.forFun + * @param {number} applicationDetails.intro.numberOfHours + * @param {string} applicationDetails.intro.whyRds + * @param {string} applicationDetails.intro.introduction + * @param {Object} applicationDetails.biodata + * @param {string} applicationDetails.biodata.firstName + * @param {string} applicationDetails.biodata.lastName + * @param {Object} applicationDetails.location + * @param {string} applicationDetails.location.country + * @param {string} applicationDetails.location.city + * @param {string} applicationDetails.location.state + * @param {string} applicationDetails.foundFrom + * @param {string} applicationDetails.userId + * @param {Object} applicationDetails.professional + * @param {string} applicationDetails.professional.skills + * @param {string} applicationDetails.professional.institution + * @param {string} applicationDetails.status - "accepted" | "rejected" | "pending" + * + * @returns {Object} validateApplicationDetails + * @returns {boolean} validateApplicationDetails.isValid + * @returns {applicationDetails} validateApplicationDetails.data + */ +export const validateApplicationDetails = (applicationDetails) => { + if (!applicationDetails || !applicationDetails.userId) { + return { isValid: false, data: null }; + } + + return { isValid: true, data: applicationDetails }; +}; diff --git a/package.json b/package.json index 30f6fcc6a..75aa78ca6 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lint:js:fix": "eslint . --fix", "dev": "concurrently \"npm:dev:*\" --names \"dev:\"", "dev:ember": "ember server -p 4200", - "dev:reverse-ssl": "local-ssl-proxy --source 443 --target 4200", + "dev:reverse-ssl": "local-ssl-proxy --source 443 --target 4200 --hostname dev.realdevsquad.com", "start": "ember serve", "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"", "test:ember": "ember test"