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
8,097 changes: 5,152 additions & 2,945 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,25 @@
"axios": "^1.4.0",
"bootstrap": "^5.3.3",
"chart.js": "^4.1.1",
"recharts": "^2.0.0",
"formik": "^2.2.9",
"i18next": "^23.4.6",
"jquery": "^3.7.1",
"jwt-decode": "^3.1.2",
"react": "^18.2.0",
"react-bootstrap": "^2.7.4",
"react-chartjs-2": "^5.2.0",
"react-datepicker": "^4.11.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"react-i18next": "^13.0.0",
"react-icons": "^4.9.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.11.1",
"react-scripts": "^5.0.1",
"recharts": "^2.0.0",
"redux-persist": "^6.0.0",
"sass": "^1.62.1",
"save": "^2.9.0",
"typescript": "^4.9.5",
"typescript": "^3.2.1",
"web-vitals": "^2.1.4",
"yup": "^1.4.0"
},
Expand Down
23 changes: 23 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ import ViewSubmissions from "pages/Assignments/ViewSubmissions";
import ViewScores from "pages/Assignments/ViewScores";
import ViewReports from "pages/Assignments/ViewReports";
import ViewDelayedJobs from "pages/Assignments/ViewDelayedJobs";
import StudentTeams from "pages/Student Teams/StudentTeamView";
import StudentTeamView from "pages/Student Teams/StudentTeamView";
import NewTeammateAdvertisement from 'pages/Student Teams/NewTeammateAdvertisement';
import TeammateReview from 'pages/Student Teams/TeammateReview';

function App() {
const router = createBrowserRouter([
{
Expand Down Expand Up @@ -106,6 +111,24 @@ function App() {
},
],
},
{
path: "student_teams",
element: <ProtectedRoute element={<StudentTeams />} />,
children: [
{
path: "view",
element: <StudentTeamView />,
},
],
},
{
path: "advertise_for_partner",
element: <ProtectedRoute element={<NewTeammateAdvertisement />} />,
},
{
path: "response/new",
element: <ProtectedRoute element={<TeammateReview />} />,
},
{
path: "users",
element: <ProtectedRoute element={<Users />} leastPrivilegeRole={ROLE.TA} />,
Expand Down
25 changes: 19 additions & 6 deletions src/hooks/useAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import { getAuthToken } from "../utils/auth";
* @author Ankur Mundra on April, 2023
*/

axios.defaults.baseURL = "http://localhost:3002/api/v1";
axios.defaults.baseURL = "http://localhost:3002";
axios.defaults.headers.common["Accept"] = "application/json";
axios.defaults.headers.post["Content-Type"] = "application/json";
axios.defaults.headers.put["Content-Type"] = "application/json";
axios.defaults.headers.patch["Content-Type"] = "application/json";

const useAPI = () => {
const [data, setData] = useState<AxiosResponse>();
const [error, setError] = useState<string>("");
const [error, setError] = useState<string | null>("");
const [errorStatus, setErrorStatus] = useState<string | null>("");
const [isLoading, setIsLoading] = useState<boolean>(true);

// Learn about Axios Request Config at https://github.com/axios/axios#request-config
Expand All @@ -38,7 +39,7 @@ const useAPI = () => {
const errors = err.response.data;
const messages = Object.entries(errors).flatMap(([field, messages]) => {
if (Array.isArray(messages)) return messages.map((m) => `${field} ${m}`);
return `${field} ${messages}`;
return `${field}: ${messages}`;
});
errorMessage = messages.join(", ");
} else if (err.request) {
Expand All @@ -51,11 +52,23 @@ const useAPI = () => {
}

if (errorMessage) setError(errorMessage);
if (err.status) setErrorStatus(err.status)
})
.finally(() => {
setIsLoading(false);
});
setIsLoading(false);
}, []);

return { data, setData, isLoading, error, sendRequest };
const reset = (error: boolean, data: boolean) => {
if (error) {
setError(null);
}
if (data) {
setData(undefined);
}
};

return { data, setData, isLoading, error, sendRequest, reset, errorStatus };
};

export default useAPI;
export default useAPI;
87 changes: 87 additions & 0 deletions src/hooks/useStudentTeam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import useAPI from "hooks/useAPI";

export const useStudentTeam = (studentId: string) => {
const teamAPI = useAPI();
const updateTeamNameAPI = useAPI();
const inviteAPI = useAPI();
const retractAPI = useAPI();
const updateInviteAPI = useAPI();
const leaveAPI = useAPI();
const fetchSentInvitationsByTeamAPI = useAPI();
const fetchSentInvitationsByParticipantAPI = useAPI();
const fetchReceivedInvitationsAPI = useAPI();

const fetchTeam = () =>
teamAPI.sendRequest({ url: `/student_teams/view?student_id=${studentId}` });

const updateName = (name: string) =>
updateTeamNameAPI.sendRequest({
method: "PUT",
url: `/student_teams/update?student_id=${studentId}`,
data: { team: { name } },
});

const createTeam = (name: string, assignment_id: number) =>
updateTeamNameAPI.sendRequest({
method: "POST",
url: `/student_teams?student_id=${studentId}`,
data: { team: { name }, assignment_id },
});

const sendInvite = (username: string, assignmentId: number) =>
inviteAPI.sendRequest({
url: `/invitations`,
method: "POST",
data: { assignment_id: assignmentId, username },
});

const retractInvite = (id: number) =>
retractAPI.sendRequest({ url: `/invitations/${id}`, method: "PATCH", data: { reply_status: "R" }, });

const updateInvite = (id: number, reply_status: "A" | "R" | "D") =>
updateInviteAPI.sendRequest({
url: `/invitations/${id}`,
method: "PATCH",
data: { reply_status },
});

const leaveTeam = () =>
leaveAPI.sendRequest({
url: `/student_teams/leave?student_id=${studentId}`,
method: "PUT",
});

const fetchSentInvitationsByTeam = (teamId: number) => fetchSentInvitationsByTeamAPI.sendRequest({
url: `/invitations/sent_by/team/${teamId}`
})

const fetchSentInvitationsByParticipant = (participantId: number) => fetchSentInvitationsByParticipantAPI.sendRequest({
url: `/invitations/sent_by/participant/${participantId}`
})

const fetchReceivedInvitations = () => fetchReceivedInvitationsAPI.sendRequest({
url: `/invitations/sent_to/${studentId}`
})

return {
teamAPI,
inviteAPI,
retractAPI,
updateInviteAPI,
updateTeamNameAPI,
leaveAPI,
fetchSentInvitationsByTeamAPI,
fetchReceivedInvitationsAPI,
fetchSentInvitationsByParticipantAPI,
fetchTeam,
createTeam,
updateName,
sendInvite,
retractInvite,
updateInvite,
fetchSentInvitationsByTeam,
fetchSentInvitationsByParticipant,
fetchReceivedInvitations,
leaveTeam,
};
};
32 changes: 32 additions & 0 deletions src/hooks/useToastHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useEffect, useRef } from "react";

export const useToastHandler = (
responses: any[],
setToastMessage: (msg: string) => void,
setShowAlert: (show: boolean) => void
) => {
// Ref to keep track of previous responses
const previousResponsesRef = useRef<any[]>([]);
useEffect(() => {
console.log(responses);
responses.forEach((res, index) => {
const prevRes = previousResponsesRef.current[index];

// Only trigger if response exists and changed
if (res && res !== prevRes) {
if (res.data) {
const { success, message } = res.data;
setShowAlert(!success);
setToastMessage(message);
}
else if (res.error) {
setShowAlert(true);
setToastMessage(res.error);
}
}
});

// Update previousResponsesRef after processing
previousResponsesRef.current = responses;
}, [responses, setToastMessage, setShowAlert]);
};
101 changes: 101 additions & 0 deletions src/pages/Student Teams/NewTeammateAdvertisement.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
.adContainer {
/* max-width: 700px; */
width: 50%;
/* margin: 40px auto; */
padding: 16px;
border: 1px solid #ccc;
border-radius: 10px;
background: #fff;
font-family: Arial, sans-serif;
}

.adList {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 12px;
}

.adListItem {
display: flex;
justify-content: space-between;
width: fit-content;
align-items: center;
background: #f3f3f3;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 6px;
/* border: 2px solid red; */
}

.adRemoveBtn {
background: none;
border: none;
color: red;
font-size: 15px;
cursor: pointer;
}

textarea {
width: 100%;
padding: 8px;
border-radius: 6px;
border: 1px solid #ccc;
resize: vertical;
}

.container {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
gap: 15px;
justify-content: center;
align-items: center;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
font-size: 1rem;
}

.header {
text-align: center;
font-size: 2rem;
}

.formLabel {
text-align: center;
font-size: 1rem;
}

.submitButton {
background-color: transparent;
border-color: #000;
color: #000;
font-size: 1rem;
padding: 2px 5px;
border-radius: 0px;
text-decoration: none;
}

.createAdButton {
background-color: transparent;
padding: 2px;
margin: 0px auto;
color: #99570c;
font-size: 1rem;
border: none;
width: fit-content;
}

.adRemoveBtn:hover,
.submitButton:hover {
cursor: pointer;
background-color: inherit;
scale: 0.99;
text-decoration: underline;
}

.createAdButton:hover {
background-color: inherit;
text-decoration: underline;
}
Loading