Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/volunteer-application-front-end #91

Merged
merged 5 commits into from
Apr 27, 2024
Merged
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
106 changes: 87 additions & 19 deletions components/Event/Event.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QueriedVolunteerEventDTO } from 'bookem-shared/src/types/database';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import Header from '@/components/Event/Header';
import BookIcon from '@/components/Event/BookIcon';
import EventName from '@/components/Event/EventName';
Expand All @@ -16,6 +16,9 @@ import {
import { Media } from '@/lib/media';
import Footer from '@/components/Event/Footer';
import { BOOKEM_THEME } from '@/utils/constants';
import { message } from 'antd';
import ApplicationPopup from './EventApplication/ApplicationPopup';

/**
* Event Detail
* @param event Data about the event
Expand All @@ -26,46 +29,95 @@ const Event = ({ event }: { event: QueriedVolunteerEventDTO }) => {
* False: display Contact
*/
const [showAbout, setShowAbout] = useState<boolean>(true);
const [showApplication, setShowApplication] = useState<boolean>(false);
const handleShowAbout = () => !showAbout && setShowAbout(!showAbout);
const handleShowContact = () => showAbout && setShowAbout(!showAbout);

/**
* Keep track of whether this event is signed up or not
*/
const [signedUp, setSignedUp] = useState(false);

/**
* keep track of the fetched application's reponse when the event requires application
*/
const [applicationResponse, setApplicationResponse] = useState(null);

useEffect(() => {
const fetchApplicationStatus = async () => {
// Fetch the submitted application
const response = await fetch(
`/api/event/${event._id}/submitted-application`,
{
method: 'GET',
}
);

// console.log("response: ", response.status)

// if the application is not found, it means the user has not applied to the event
// do nothing
if (response.status === 404 || response.status === 400) {
return;
}

const { application: eventApplication, response: applicationResponse } =
await response.json();

// console.log("application: ", eventApplication)
// console.log("response: ", applicationResponse)

// If the application is submitted, set signedUp to true
setSignedUp(true);

// set the application response
setApplicationResponse(applicationResponse);
};

if (event.applicationId != null) {
fetchApplicationStatus();
}
}, [event._id, event.applicationId]);

/**
* Sign up/Unsign up the current user to the event
* @param event
*/
const signUpEvent = async () => {
try {
// If the event requires application, redirect to application page
if (event.requireApplication) {
// TODO: redirect to event application page
alert('Go to event application!');
if (event.applicationId != null) {
if (signedUp) {
message.info('You have already applied to this event');
return;
}
setShowApplication(true);
// console.log("application questions: ", event.applicationId)

return;
}
} else {
// Send POST request to sign up
const response = await fetch('/api/event/' + event._id, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});

// Send POST request to sign up
const response = await fetch('/api/event/' + event._id, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});

// Success
if (response.status === 200) {
const message = await response.json();
console.log(message);
// Update sign up state
setSignedUp(!signedUp);
// Success
if (response.status === 200) {
const message = await response.json();
// console.log(message);
// Update sign up state
setSignedUp(!signedUp);
}
}
} catch (error) {
console.error(error);
}
};

console.log('event: ', event);
return (
<>
<EventBox>
Expand All @@ -82,6 +134,7 @@ const Event = ({ event }: { event: QueriedVolunteerEventDTO }) => {
setSignedUp={setSignedUp}
event={event}
signUpEvent={signUpEvent}
applicationResponses={applicationResponse}
/>
</MiddleBox>

Expand Down Expand Up @@ -142,6 +195,21 @@ const Event = ({ event }: { event: QueriedVolunteerEventDTO }) => {
signUpEvent={signUpEvent}
/>
</Media>

{/* Application Popup */}
{event.applicationId != null && (
<ApplicationPopup
event={event}
visible={showApplication}
onClose={() => {
setShowApplication(false);
}}
onSubmit={() => {
message.success('Application Submitted');
setSignedUp(true);
}}
/>
)}
</>
);
};
Expand Down
130 changes: 130 additions & 0 deletions components/Event/EventApplication/ApplicationPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Modal } from 'antd';
import 'survey-core/defaultV2.min.css';
import { Model } from 'survey-core';
import { Survey } from 'survey-react-ui';
import { useState, useEffect } from 'react';
import {
ApplicationQuestionData,
ApplicationAnswer,
QueriedVolunteerEventDTO,
} from 'bookem-shared/src/types/database';

// choices can type string[] or null
const processChoices = (choices: String[] | undefined) => {
if (choices == null) {
return null;
}

return choices.map(choice => {
return {
value: choice,
text: choice,
};
});
};

// process answes to fit the answer type. return the answer casted into application answer
const processAnswers = (answerObj: any) => {
// the key of the answer object is the question id
// the value is the answer. The value can be a string or an array of strings
// we need to convert this into an array of application answers
const answers = Object.keys(answerObj).map(key => {
const answer = answerObj[key];
return {
questionId: key,
// check if the answer is an array, if not convert it into an array
text: Array.isArray(answer) ? answer : [answer],
};
});

return answers;
};

const makeSurveyModel = (questions: ApplicationQuestionData[]) => {
const elements = questions.map(question => {
return {
name: question._id,
title: question.title,
type: question.type,
isRequired: question.isRequired,
choices: processChoices(question.choices),
};
});

return {
elements: elements,
};
};

export default function ApplicationPopup({
visible,
onClose,
event,
onSubmit,
}: {
visible: boolean;
onClose: () => void;
event: QueriedVolunteerEventDTO;
onSubmit: () => void;
}) {
const { name } = event;
const [questions, setQuestions] = useState<ApplicationQuestionData[]>([]);

let survey = new Model(makeSurveyModel(questions));

survey.onComplete.add(result => {
const answers = processAnswers(result.data);
console.log('Survey results: ', answers);
// send the answers to the server
fetch('/api/event/' + event._id + '/apply', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
answers: answers,
}),
});

onClose();
onSubmit();
});

useEffect(() => {
const fetchQuestions = async () => {
const res = await fetch('/api/event/' + event._id + '/apply', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

// no application questions found
if (res.status === 404) {
return;
}

const data = await res.json();
const questions = data.message;
setQuestions(questions);
};
// fetch the questions from the database
// set the survey model
fetchQuestions();
}, [event._id]);

return (
<Modal
width={800}
// title="Application"
open={visible}
onCancel={onClose}
footer={null}>
<h2>
Application for event: <br />
<span style={{ color: 'gray', fontSize: '1rem' }}>{name}</span>
</h2>
<Survey model={survey} />
</Modal>
);
}
78 changes: 78 additions & 0 deletions components/Event/EventApplication/ApplicationStatusDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { StatusDisplayText } from '@/styles/components/Event/eventName.styles';
import {
CheckCircleOutlined,
ReadOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { useState, useEffect } from 'react';
import {
ApplicationStatus,
ApplicationResponseData,
QueriedVolunteerEventDTO,
} from 'bookem-shared/src/types/database';

const ApprovedText = () => (
<span>
<CheckCircleOutlined
style={{
color: 'green',
margin: '0 10px',
}}
/>
Approved
</span>
);

const PendingText = () => (
<span>
<ReadOutlined
style={{
color: 'orange',
margin: '0 10px',
}}
/>
Under Review
</span>
);

const RejectedText = () => (
<span>
<ExclamationCircleOutlined
style={{
color: 'red',
margin: '0 10px',
}}
/>
Rejected
</span>
);

const statusDisplayText = {
[ApplicationStatus.Pending]: <PendingText />,
[ApplicationStatus.Approved]: <ApprovedText />,
[ApplicationStatus.Rejected]: <RejectedText />,
};

// Display the status of the volunteer application
// TODO: add logic for fetching application responses and display its status
const ApplicationStatusDisplay = ({
event,
applicationResponses,
}: {
event: QueriedVolunteerEventDTO;
applicationResponses: ApplicationResponseData | null;
}) => {
if (applicationResponses?.status == null) {
return <StatusDisplayText>Application Required</StatusDisplayText>;
}

return (
<StatusDisplayText>
Application Status:
<br />
{statusDisplayText[applicationResponses.status]}
</StatusDisplayText>
);
};

export default ApplicationStatusDisplay;
Loading
Loading