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 pre-/post-requisite tree under each class #188

Merged
merged 70 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
1fe1bab
create component ReqTreeCard.tsx
aattiyah Dec 4, 2024
77252cd
create card for pre.post.corequisite tree
aattiyah Dec 4, 2024
99d6421
add comments
aattiyah Dec 4, 2024
a85c07f
import ReqTreeCard into CourseDetail.tsx
aattiyah Dec 4, 2024
90cdc26
add card to course details
aattiyah Dec 4, 2024
62757ec
change heading title of ReqTreeCard
aattiyah Dec 4, 2024
cad2d2d
add guiding comments
aattiyah Dec 4, 2024
80c0f9f
Add postrequisites fetching
mohamed-elzeni Dec 9, 2024
25d847a
Add postreqs to tree card
mohamed-elzeni Dec 9, 2024
8291273
Update ReqTreeCard to render the prerequisite list
akobaidan Dec 9, 2024
d282973
Create ReqTreeDetail to render the tree diagram
akobaidan Dec 9, 2024
4819f35
Update ReqTreeCard to build tree diagram and CourseDetail to fetch data
akobaidan Dec 9, 2024
5e36e55
Update ReqTreeDetail for UI enhancements and link courses to pages
akobaidan Dec 9, 2024
1c68659
Updated ReqTreeCard to display message if tree unavailable
akobaidan Dec 9, 2024
ebbb419
Merge pull request #1 from mohamed-elzeni/add-tree-diagram
akobaidan Dec 9, 2024
101a1f5
slight bug fixes and the addetion to an expanding pre req tree
Dec 9, 2024
e33d68b
adjusting the arrow feature aand adding a collapse
Dec 9, 2024
8c158e0
adjusting the arrow feature and fixing the bugs in it
Dec 9, 2024
b967bcc
fixing the bugs in it
Dec 9, 2024
d79cff6
add PostReqCourses for tree
aattiyah Dec 9, 2024
7dd2456
modify ReqTreeDetail for more postreq courses
aattiyah Dec 9, 2024
7c7c496
link ReqTreeCard for more postreq courses
aattiyah Dec 9, 2024
47fb612
properly adding the view more feature
Dec 9, 2024
14ede1c
fixing it slightly
Dec 9, 2024
380ff71
organize visualization of buttons
aattiyah Dec 9, 2024
554b2a9
organize the post-reqs of post-reqs
aattiyah Dec 9, 2024
3688001
remove redudant code
aattiyah Dec 9, 2024
b4bd11d
resolving conflicts
Dec 9, 2024
bf146fd
resolving conflicts
Dec 9, 2024
16babe7
resolving conflicts
Dec 10, 2024
e20f5e0
resolving conflicts
Dec 10, 2024
8395f96
resolving conflicts
Dec 10, 2024
79b60f6
resolving conflicts
Dec 10, 2024
09ecb36
resolving conflicts
Dec 10, 2024
0aaad61
resolving conflicts
Dec 10, 2024
79252e9
resolving conflicts
Dec 10, 2024
0b6b2b7
resolving minor bugs
Dec 10, 2024
b77dcde
resolving minor bugs
Dec 10, 2024
91a9c75
Merge branch 'main' into prereqtree
lhitmi Dec 10, 2024
913d0bd
add lines to the postreq nodes
aattiyah Dec 10, 2024
0b1a467
Merge pull request #2 from mohamed-elzeni/add-tree-diagram
aattiyah Dec 10, 2024
6fc686a
adding the link to the traversal of prereq
Dec 10, 2024
f6d22dd
adjusting imports and functionality
Dec 10, 2024
a0f01d5
adding corereq to the req card
Dec 10, 2024
426cd74
fixing bugs
Dec 10, 2024
e352eb1
updating changes
Dec 10, 2024
760978d
Merge branch 'main' into prereqtree
lhitmi Dec 10, 2024
6884b59
centered course req
Dec 10, 2024
6fbcbe7
updating changes
Dec 10, 2024
a75d38d
fixing rendering issue
Dec 10, 2024
21021d3
shifting the view more to the left side
Dec 11, 2024
5d45e93
Merge pull request #3 from mohamed-elzeni/prereqtree
lhitmi Dec 11, 2024
b2968ad
added parser function in utils.ts for prereqString
Dec 11, 2024
8b6ae54
added a function that get the postreqs and prereqs elements of the tree
Dec 11, 2024
175b792
added a new route for course/relations endpoint
Dec 11, 2024
a315151
fix coreqs placement
aattiyah Dec 11, 2024
85cf02e
fix pre-req placements
aattiyah Dec 11, 2024
f4a66ad
Merge pull request #4 from mohamed-elzeni/treefunction
mohamed-elzeni Dec 12, 2024
936a778
Enhance tree branches
mohamed-elzeni Dec 12, 2024
3ad0863
Minor requisite tree branches fix
mohamed-elzeni Dec 12, 2024
d092d75
update style consistency using trailwind
aattiyah Dec 12, 2024
ed85d6e
Update requisites fetching
mohamed-elzeni Dec 13, 2024
51c89f2
Requisite tree UI enhancements
mohamed-elzeni Dec 13, 2024
775832b
Standardize tree interface
mohamed-elzeni Dec 13, 2024
2c6d7cb
change labels
aattiyah Dec 14, 2024
9ff32b3
Enhance requisites tree UI
mohamed-elzeni Dec 14, 2024
e95d672
Fix tree branches
mohamed-elzeni Dec 14, 2024
0b6c7e0
Add expanding multiple courses simultaneously
mohamed-elzeni Dec 14, 2024
f452805
Refactor requsisites tree code
mohamed-elzeni Dec 14, 2024
6740e36
Minor refactoring
mohamed-elzeni Dec 14, 2024
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
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
Loading