Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/constants/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const APPLICATION_STATUS_TYPES = {
accepted: 'accepted',
rejected: 'rejected',
pending: 'pending',
};
147 changes: 147 additions & 0 deletions app/controllers/intro.js
Original file line number Diff line number Diff line change
@@ -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,
);
});
}
}
24 changes: 17 additions & 7 deletions app/routes/intro.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
1 change: 1 addition & 0 deletions app/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 46 additions & 0 deletions app/styles/button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
18 changes: 18 additions & 0 deletions app/styles/feedback.css
Original file line number Diff line number Diff line change
@@ -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;
}
69 changes: 58 additions & 11 deletions app/templates/intro.hbs
Original file line number Diff line number Diff line change
@@ -1,18 +1,65 @@
{{page-title "Intro"}}

<section class="intro__container">
{{#if this.login.isLoading}}
<div data-test-loading class='intro__loading'>
<Fa-Icon @size='2x' @icon='circle-notch' @spin={{true}} />
<div data-test-loading class="intro__loading">
<Fa-Icon @size="2x" @icon="circle-notch" @spin={{true}} />
</div>
{{else}}
{{#if this.login.userData.roles.super_user}}
<ApplicantIntro
@data={{this.model}}
{{else if this.applicationDetails}}
{{#if this.isAccepted}}
<div>
<p>Congratulations. You've been approved to join our Discord server using this link:</p>
<a href="{{this.inviteLink}}" target="_blank" rel="noopener noreferrer">{{this.inviteLink}}</a>
<button type="button" class="copy-button" {{on "click" (fn this.copyToClipboard this.inviteLink)}}>Copy Link</button>
<p>{{this.feedback}}</p>
</div>
{{else if this.isRejected}}
<div>
<p>Unfortunately, at this time, your application is not approved.</p>
<p>{{this.feedback}}</p>
</div>
{{else if this.isPending}}
<div>We are reviewing your application. Please check again here after some time for an update.</div>
{{else if this.login.userData.roles.super_user}}
<ApplicantIntro
@data={{get this.model.data 0}}
@isLoading={{this.login.isLoading}}
@isSuperUser={{this.login.userData.roles.super_user}} />
@isSuperUser={{this.login.userData.roles.super_user}}
/>

<div class="superuser-feedback">
<label for="superuserFeedback" class="superuser-feedback__label">
Your remarks:
</label>
<textarea
id="superuserFeedback"
class="superuser-feedback__textarea"
placeholder="Enter your remarks"
{{on "change" this.updateRemarks}}
></textarea>
</div>

<div class="action-buttons">
<button
type="button"
class="approve-button"
{{on "click" (fn this.approveRejectAction this.statusTypes.accepted)}}
>Accept</button>

<button
type="button"
class="reject-button"
{{on "click" (fn this.approveRejectAction this.statusTypes.rejected)}}
>Reject</button>

<button
type="button"
class="pending-button"
{{on "click" (fn this.approveRejectAction this.statusTypes.pending)}}
>Pending</button>
</div>
{{else}}
<Unauthorized />
<div>You're not authorized to view this page.</div>
{{/if}}
{{else}}
<div>No application details found.</div>
{{/if}}
</section>
</section>
36 changes: 36 additions & 0 deletions app/utils/validate-application-details.js
Original file line number Diff line number Diff line change
@@ -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 };
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down