Skip to content

Commit

Permalink
Add pre-/post-requisite tree under each class (#188)
Browse files Browse the repository at this point in the history
* create component ReqTreeCard.tsx

* create card for pre.post.corequisite tree

* add comments

* import ReqTreeCard into CourseDetail.tsx

* add card to course details

* change heading title of ReqTreeCard

* add guiding comments

* Add postrequisites fetching

* Add postreqs to tree card

* Update ReqTreeCard to render the prerequisite list

* Create ReqTreeDetail to render the tree diagram

* Update ReqTreeCard to build tree diagram and CourseDetail to fetch data

* Update ReqTreeDetail for UI enhancements and link courses to pages

* Updated ReqTreeCard to display message if tree unavailable

* slight bug fixes and the addetion to an expanding pre req tree

* adjusting the arrow feature aand adding a collapse

* adjusting the arrow feature and fixing the bugs in it

* fixing the bugs in it

* add PostReqCourses for tree

* modify ReqTreeDetail for more postreq courses

* link ReqTreeCard for more postreq courses

* properly adding the view more feature

* fixing it slightly

* organize visualization of buttons

* organize the post-reqs of post-reqs

* remove redudant code

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving minor bugs

* resolving minor bugs

* add lines to the postreq nodes

* adding the link to the traversal of prereq

* adjusting imports and functionality

* adding corereq to the req card

* fixing bugs

* centered course req

* fixing rendering issue

* shifting the view more to the left side

* added parser function in utils.ts for prereqString

* added a function that get the postreqs and prereqs elements of the tree

* added a new route for course/relations endpoint

* fix coreqs placement

* fix pre-req placements

* Enhance tree branches

* Minor requisite tree branches fix

* update style consistency using trailwind

* Update requisites fetching

* Requisite tree UI enhancements

* Standardize tree interface

* change labels

* Enhance requisites tree UI

* Fix tree branches

* Add expanding multiple courses simultaneously

* Refactor requsisites tree code

* Minor refactoring

---------

Co-authored-by: Mohamed Elzeni <[email protected]>
Co-authored-by: “akobaidan” <[email protected]>
Co-authored-by: akobaidan <[email protected]>
Co-authored-by: Latifa Al-Hitmi <[email protected]>
Co-authored-by: lhitmi <[email protected]>
Co-authored-by: Fatou GUEYE <[email protected]>
Co-authored-by: Mohamed Elzeni <[email protected]>
  • Loading branch information
8 people authored Dec 15, 2024
1 parent e4e0c4e commit cbfef27
Show file tree
Hide file tree
Showing 11 changed files with 528 additions and 7 deletions.
3 changes: 2 additions & 1 deletion apps/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import morgan from "morgan";
import express, { ErrorRequestHandler } from "express";
import cors from "cors";
import { isUser } from "~/controllers/user";
import { getAllCourses, getCourseByID, getCourses, getFilteredCourses } from "~/controllers/courses";
import { getAllCourses, getCourseByID, getCourses, getFilteredCourses, getRequisites } from "~/controllers/courses";
import { getFCEs } from "~/controllers/fces";
import { getInstructors } from "~/controllers/instructors";
import { getGeneds } from "~/controllers/geneds";
Expand All @@ -21,6 +21,7 @@ app.route("/course/:courseID").get(getCourseByID);
app.route("/courses").get(getCourses);
app.route("/courses").post(isUser, getCourses);
app.route("/courses/all").get(getAllCourses);
app.route("/courses/requisites/:courseID").get(getRequisites);
app.route("/courses/search/").get(getFilteredCourses);
app.route("/courses/search/").post(isUser, getFilteredCourses);

Expand Down
50 changes: 50 additions & 0 deletions apps/backend/src/controllers/courses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SingleOrArray,
singleToArray,
standardizeID,
parsePrereqString,
} from "~/util";
import { RequestHandler } from "express";
import db, { Prisma } from "@cmucourses/db";
Expand Down Expand Up @@ -272,3 +273,52 @@ export const getAllCourses: RequestHandler<
res.json(allCoursesEntry.allCourses);
}
};


export const getRequisites: RequestHandler = async (req, res, next) => {
try {
if (!req.params.courseID) {
return res.status(400).json({ error: 'courseID parameter is required' });
}

const courseID = standardizeID(req.params.courseID);

const course = await db.courses.findUnique({
where: { courseID },
select: {
courseID: true,
prereqs: true,
prereqString: true,
},
});

if (!course) {
return res.status(400).json({ error: 'Course not found' });
}

const parsedPrereqs = parsePrereqString(course.prereqString);

const postreqs = await db.courses.findMany({
where: {
prereqs: {
has: course.courseID,
},
},
select: {
courseID: true,
},
});

const postreqIDs = postreqs.map(postreq => postreq.courseID);

const courseRequisites = {
prereqs: course.prereqs,
prereqRelations: parsedPrereqs,
postreqs: postreqIDs
}

res.json(courseRequisites);
} catch (e) {
next(e);
}
};
8 changes: 7 additions & 1 deletion apps/backend/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ export type PrismaReturn<PrismaFnType extends (...args: any) => any> =
Awaited<ReturnType<PrismaFnType>>;

export type ElemType<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;

export function parsePrereqString(prereqString: string): string[][] {
const normalized = prereqString.replace(/\s+/g, "").replace(/[()]/g, ""); // Remove whitespace and parentheses
const andGroups = normalized.split("and"); // Split by AND groups
return andGroups.map((group) => group.split("or")); // Split each AND group into OR relationships
}
26 changes: 26 additions & 0 deletions apps/frontend/src/app/api/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,29 @@ export const useFetchAllCourses = () => {
staleTime: STALE_TIME,
});
};

export type CourseRequisites = {
prereqs: string[];
prereqRelations: string[][];
postreqs: string[];
};

export const fetchCourseRequisites = async (courseID: string): Promise<CourseRequisites> => {
const url = `${process.env.NEXT_PUBLIC_BACKEND_URL || ""}/courses/requisites/${courseID}`;

const response = await axios.get(url, {
headers: {
"Content-Type": "application/json",
},
});

return response.data;
};

export const useFetchCourseRequisites = (courseID: string) => {
return useQuery<CourseRequisites>({
queryKey: ['courseRequisites', courseID],
queryFn: () => fetchCourseRequisites(courseID),
staleTime: STALE_TIME,
});
};
6 changes: 6 additions & 0 deletions apps/frontend/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,9 @@ export interface Gened {
fces: FCE[];
}

export interface TreeNode {
courseID: string;
prereqs?: TreeNode[];
prereqRelations?: TreeNode[][];
postreqs?: TreeNode[];
}
15 changes: 15 additions & 0 deletions apps/frontend/src/components/Buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,18 @@ export const FlushedButton = ({
</div>
);
};

export const CourseIDButton = ({
courseID,
}: {
courseID: string;
}) => {
return (
<button
onClick={() => (window.location.href = `/course/${courseID}`)}
className="font-normal text-center px-2 py-1 text-base bg-gray-50 hover:bg-gray-200 text-gray-900 border border-gray-300 rounded shadow cursor-pointer no-underline min-w-20 inline mt-1 mb-1"
>
{courseID}
</button>
)
}
24 changes: 19 additions & 5 deletions apps/frontend/src/components/CourseDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,41 @@ import CourseCard from "./CourseCard";
import { useFetchFCEInfoByCourse } from "~/app/api/fce";
import { SchedulesCard } from "./SchedulesCard";
import { FCECard } from "./FCECard";
import { useFetchCourseInfo } from "~/app/api/course";
import { useFetchCourseInfo, useFetchCourseRequisites } from "~/app/api/course";
import ReqTreeCard from "./ReqTreeCard";

type Props = {
courseID: string;
};

const CourseDetail = ({ courseID }: Props) => {
const { data: { fces } = {} } = useFetchFCEInfoByCourse(courseID);
const { data: { schedules } = {} } = useFetchCourseInfo(courseID);
const { data: info } = useFetchCourseInfo(courseID);
const { data: requisites } = useFetchCourseRequisites(courseID);

if (!info || !requisites) {
return <div>Loading...</div>;
}

return (
<div className="m-auto space-y-4 p-6">
<CourseCard courseID={courseID} showFCEs={false} showCourseInfo={true} />
{fces && <FCECard fces={fces} />}
{schedules && (
{info.schedules && (
<SchedulesCard
scheduleInfos={filterSessions([...schedules]).sort(compareSessions)}
scheduleInfos={filterSessions([...info.schedules]).sort(compareSessions)}
/>
)}
{info.prereqs && requisites.prereqRelations && requisites.postreqs && (
<ReqTreeCard
courseID={courseID}
prereqs={requisites.prereqs}
prereqRelations={requisites.prereqRelations}
postreqs={requisites.postreqs}
/>
)}
</div>
);
};

export default CourseDetail;
export default CourseDetail;
80 changes: 80 additions & 0 deletions apps/frontend/src/components/PostReqCourses.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from "react";
import { useFetchCourseRequisites } from "~/app/api/course";
import { TreeNode } from "~/app/types";
import { CourseIDButton } from "./Buttons";

interface Props {
courseID: string;
}

export const PostReqCourses = ({ courseID }: Props) => {
const { isPending: isCourseInfoPending, data: requisites } = useFetchCourseRequisites(courseID);

if (isCourseInfoPending || !requisites) {
return null;
}

// Recursive function to render only the child branches
const renderTree = (nodes: TreeNode[]) => {
return (
<div className="flex flex-col">
{nodes.map((node) => (
<div key={node.courseID} className="flex items-center">
{/* Half vertical line for the first postreq */}
{nodes && nodes.length > 1 && nodes.indexOf(node) === 0 && (
<div className="flex flex-col w-0.5 self-stretch">
<div className="h-1/2 self-stretch"></div>
<div className="w-0.5 h-1/2 bg-gray-400 self-stretch"></div>
</div>
)}

{/* Normal vertical Line connector */}
{nodes && nodes.length > 1 && nodes.indexOf(node) !== 0 && nodes.indexOf(node) !== nodes.length - 1 && (
<div className="w-0.5 bg-gray-400 self-stretch"></div>
)}

{/* Half vertical line for the last prereq in the list */}
{nodes && nodes.length > 1 && nodes.indexOf(node) === nodes.length - 1 && (
<div className="flex flex-col w-0.5 self-stretch">
<div className="w-0.5 h-1/2 bg-gray-400 self-stretch"></div>
<div className="h-1/2 self-stretch"></div>
</div>
)}

{/* Line left to node */}
{nodes && nodes.length > 1 && (
<div className="w-3 h-0.5 bg-gray-400"></div>
)}

{/* Course ID button */}
<CourseIDButton courseID={node.courseID} />

{/* Render child nodes recursively */}
{node.postreqs && renderTree(node.postreqs)}
</div>
))}
</div>
);
};

// Transform fetched data into a tree structure excluding the parent node
const childNodes: TreeNode[] = requisites.postreqs?.map((postreq: string) => ({
courseID: postreq,
})) || [];

return (
<div>
{childNodes.length > 0 ? (
renderTree(childNodes)
) : (
<div
className="italic ml-2 text-gray-700 text-center text-lg font-semibold rounded-md"
>
None
</div>
)}
</div>
);
};

export default PostReqCourses;
80 changes: 80 additions & 0 deletions apps/frontend/src/components/PreReqCourses.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from "react";
import { useFetchCourseRequisites } from "~/app/api/course";
import { TreeNode } from "~/app/types";
import { CourseIDButton } from "./Buttons";

interface Props {
courseID: string;
}

export const PreReqCourses = ({ courseID }: Props) => {
const { isPending: isCourseInfoPending, data: requisites } = useFetchCourseRequisites(courseID);

if (isCourseInfoPending || !requisites) {
return null;
}

// Recursive function to render only the child branches
const renderTree = (nodes: TreeNode[]) => {
return (
<div className="flex flex-col">
{nodes.map((node) => (
<div key={node.courseID} className="flex items-center">
{/* Course ID button */}
<CourseIDButton courseID={node.courseID} />

{/* Line connector right to node */}
{nodes && nodes.length > 1 && (
<div className="w-3 h-0.5 bg-gray-400"></div>
)}

{/* Half vertical line for the first prereq in the list */}
{nodes && nodes.length > 1 && nodes.indexOf(node) === 0 && (
<div className="flex flex-col w-0.5 self-stretch">
<div className="h-1/2 self-stretch"></div>
<div className="w-0.5 h-1/2 bg-gray-400 self-stretch"></div>
</div>
)}

{/* Normal vertical Line connector */}
{nodes && nodes.length > 1 && nodes.indexOf(node) !== 0 && nodes.indexOf(node) !== nodes.length - 1 && (
<div className="w-0.5 bg-gray-400 self-stretch"></div>
)}

{/* Half vertical line for the last prereq in the list */}
{nodes && nodes.length > 1 && nodes.indexOf(node) === nodes.length - 1 && (
<div className="flex flex-col w-0.5 self-stretch">
<div className="w-0.5 h-1/2 bg-gray-400 self-stretch"></div>
<div className="h-1/2 self-stretch"></div>
</div>
)}

{/* Render child nodes recursively */}
{node.prereqs && renderTree(node.prereqs)}
</div>
))}
</div>
);
};

// Transform fetched data into a tree structure excluding the parent node
const childNodes: TreeNode[] = requisites.prereqs.map((prereq: string) => ({
courseID: prereq,
})) || [];

return (
<div>
{childNodes.length > 0 ? (
renderTree(childNodes)
) : (
<div
className="italic mr-2 text-gray-700 text-center text-lg font-semibold rounded-md"
>
None
</div>
)}
</div>
);
};

export default PreReqCourses;
Loading

0 comments on commit cbfef27

Please sign in to comment.