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

Add Participants feature to Assignment and Courses page #17

Merged
merged 20 commits into from
Jan 24, 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
37 changes: 37 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import ErrorPage from "./router/ErrorPage";
import NotFound from "./router/NotFound";
import ProtectedRoute from "./router/ProtectedRoute";
import { ROLE } from "./utils/interfaces";
import AdministratorLayout from "./layout/Administrator";
import NotFound from "./router/NotFound";
import Participants from "pages/Participants/Participant";
import ParticipantEditor from "pages/Participants/ParticipantEditor";
import { loadParticipantDataRolesAndInstitutions } from "pages/Participants/participantUtil";
import RootLayout from "layout/Root";
import UserEditor from "./pages/Users/UserEditor";
import Users from "./pages/Users/User";
Expand All @@ -27,6 +32,7 @@ import TA from "pages/TA/TA";
import TAEditor from "pages/TA/TAEditor";
import { loadTAs } from "pages/TA/TAUtil";


function App() {
const router = createBrowserRouter([
{
Expand Down Expand Up @@ -71,6 +77,37 @@ function App() {
],
},
{
path: "student_tasks/participants",
// TODO: The id here should be dynamic and should be received from the parent component
element: <Participants type="student_tasks" id={1} />,
children: [
{
path: "new",
element: <ParticipantEditor mode="create" type="student_tasks" />,
loader: loadParticipantDataRolesAndInstitutions,
},
{
path: "edit/:id",
element: <ParticipantEditor mode="update" type="student_tasks" />,
loader: loadParticipantDataRolesAndInstitutions,
},
]
},
{
path: "courses/participants",
// TODO: The id here should be dynamic and should be received from the parent component
element: <Participants type="courses" id={1} />,
children: [
{
path: "new",
element: <ParticipantEditor mode="create" type="courses" />,
loader: loadParticipantDataRolesAndInstitutions,
},
{
path: "edit/:id",
element: <ParticipantEditor mode="update" type="courses" />,
loader: loadParticipantDataRolesAndInstitutions,
},
// Routing for courses, so the URL will be https://<domain>.com/courses
// This route is protected and only TAs can view it.
path: "courses",
Expand Down
109 changes: 109 additions & 0 deletions src/pages/Participants/Participant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Row as TRow } from "@tanstack/react-table";
import Table from "components/Table/Table";
import useAPI from "hooks/useAPI";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button, Col, Container, Row } from "react-bootstrap";
import { BsPersonFillAdd } from "react-icons/bs";
import { useDispatch, useSelector } from "react-redux";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { alertActions } from "store/slices/alertSlice";
import { RootState } from "../../store/store";
import { IParticipantResponse, ROLE } from "../../utils/interfaces";
import DeleteParticipant from "./ParticipantDelete";
import { participantColumns as PARPTICIPANT_COLUMNS } from "./participantColumns";

/**
* @author Atharva Thorve on October, 2023
*/

interface IModel {
type: "student_tasks" | "courses";
id: Number;
}

const Participants: React.FC<IModel> = ({ type, id }) => {
const { error, isLoading, data: participantResponse, sendRequest: fetchParticipants } = useAPI();
const auth = useSelector(
(state: RootState) => state.authentication,
(prev, next) => prev.isAuthenticated === next.isAuthenticated
);
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();

const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{
visible: boolean;
data?: IParticipantResponse;
}>({ visible: false });

useEffect(() => {
if (!showDeleteConfirmation.visible) fetchParticipants({ url: `/participants/${type}/${id}` });
}, [fetchParticipants, location, showDeleteConfirmation.visible, auth.user.id, type, id]);

// Error alert
useEffect(() => {
if (error) {
dispatch(alertActions.showAlert({ variant: "danger", message: error }));
}
}, [error, dispatch]);

const onDeleteParticipantHandler = useCallback(() => setShowDeleteConfirmation({ visible: false }), []);

const onEditHandle = useCallback(
(row: TRow<IParticipantResponse>) => navigate(`/${type}/participant/edit/${row.original.id}`),
[navigate, type]
);

const onDeleteHandle = useCallback(
(row: TRow<IParticipantResponse>) => setShowDeleteConfirmation({ visible: true, data: row.original }),
[]
);

const tableColumns = useMemo(
() => PARPTICIPANT_COLUMNS(onEditHandle, onDeleteHandle),
[onDeleteHandle, onEditHandle]
);

const tableData = useMemo(
() => (isLoading || !participantResponse?.data ? [] : participantResponse.data),
[participantResponse?.data, isLoading]
);

return (
<>
<Outlet />
<main>
<Container fluid className="px-md-4">
<Row className="mt-md-2 mb-md-2">
<Col className="text-center">
<h1>Manage Participants</h1>
</Col>
<hr />
</Row>
<Row>
<Col md={{ span: 1, offset: 11 }}>
<Button variant="outline-success" onClick={() => navigate("new")}>
<BsPersonFillAdd />
</Button>
</Col>
{showDeleteConfirmation.visible && (
<DeleteParticipant participantData={showDeleteConfirmation.data!} onClose={onDeleteParticipantHandler} />
)}
</Row>
<Row>
<Table
data={tableData}
columns={tableColumns}
columnVisibility={{
id: false,
institution: auth.user.role === ROLE.SUPER_ADMIN.valueOf(),
}}
/>
</Row>
</Container>
</main>
</>
);
};

export default Participants;
73 changes: 73 additions & 0 deletions src/pages/Participants/ParticipantDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useEffect, useState } from "react";
import { Button, Modal } from "react-bootstrap";
import { useDispatch } from "react-redux";
import { alertActions } from "store/slices/alertSlice";
import { HttpMethod } from "utils/httpMethods";
import useAPI from "../../hooks/useAPI";
import { IParticipantResponse as IParticipant } from "../../utils/interfaces";

/**
* @author Atharva Thorve on October, 2023
*/

interface IDeleteParticipant {
participantData: IParticipant;
onClose: () => void;
}

const DeleteParticipant: React.FC<IDeleteParticipant> = ({ participantData, onClose }) => {
const { data: deletedParticipant, error: participantError, sendRequest: deleteParticipant } = useAPI();
const [show, setShow] = useState<boolean>(true);
const dispatch = useDispatch();

// Delete user
const deleteHandler = () =>
deleteParticipant({ url: `/participants/${participantData.id}`, method: HttpMethod.DELETE });

// Show error if any
useEffect(() => {
if (participantError) dispatch(alertActions.showAlert({ variant: "danger", message: participantError }));
}, [participantError, dispatch]);

// Close modal if user is deleted
useEffect(() => {
if (deletedParticipant?.status && deletedParticipant?.status >= 200 && deletedParticipant?.status < 300) {
setShow(false);
dispatch(
alertActions.showAlert({
variant: "success",
message: `User ${participantData.name} deleted successfully!`,
})
);
onClose();
}
}, [deletedParticipant?.status, dispatch, onClose, participantData.name]);

const closeHandler = () => {
setShow(false);
onClose();
};

return (
<Modal show={show} onHide={closeHandler}>
<Modal.Header closeButton>
<Modal.Title>Delete Participant</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>
Are you sure you want to delete participant <b>{participantData.name}?</b>
</p>
</Modal.Body>
<Modal.Footer>
<Button variant="outline-secondary" onClick={closeHandler}>
Cancel
</Button>
<Button variant="outline-danger" onClick={deleteHandler}>
Delete
</Button>
</Modal.Footer>
</Modal>
);
};

export default DeleteParticipant;
Loading