diff --git a/src/App.tsx b/src/App.tsx index 5b175fd..24bc137 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,21 +2,28 @@ import { useState } from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import Catalog from "./pages/Catalog"; import Planner from "./pages/Planner"; -import DepartmentFilters from "./components/Department-Filters.tsx"; import Toolbox from "./components/Toolbox/Toolbox"; import { CourseEntry, CourseType, } from "./types/interfaces/Course.interface.ts"; import { DragDropContext, DropResult } from "@hello-pangea/dnd"; +import { SemesterType } from "./types/interfaces/Semester.interface.ts"; import { Filters } from "./types/Filters"; function App() { // Original state from App.tsx const [toolboxCourses, setToolboxCourses] = useState([]); + const [plannerCourses, setPlannerCourses] = useState([ + { + semesterNumber: 1, + semesterSeason: "fall", + creditsTotal: 0, + courseList: [], + }, + ]); const [isDragging, setIsDragging] = useState(false); - // Moved from Catalog.tsx const [searchResults, setSearchResults] = useState([]); // Moved from SearchBar.tsx @@ -28,83 +35,282 @@ function App() { Semesters: [], }); - const reorder = ( - list: CourseEntry[], + // Reorders a simple array + const reorder = ( + list: T[], startIndex: number, - endIndex: number, - ) => { - const result = Array.from(list); + endIndex: number + ): T[] => { + const result = [...list]; const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); - return result; }; - const deleteCourse = (list: CourseEntry[], startIndex: number) => { - const result = Array.from(list); - result.splice(startIndex, 1); + const cloneCourse = (course: CourseEntry): CourseEntry => { + return { + ...course, + name: course.name + "-" + Math.random().toString(36).substring(2, 7), + data: { ...course.data }, + count: 1, + }; + }; - return result; + const onDragStart = () => { + setIsDragging(true); }; const onDragEnd = (result: DropResult) => { setIsDragging(false); - const { source, destination } = result; + const { source, destination, draggableId } = result; if (!destination) return; const sInd = source.droppableId; const dInd = destination.droppableId; - if (sInd === dInd && sInd === "toolbox") { - const items = reorder(toolboxCourses, source.index, destination.index); - setToolboxCourses(items); - } else if (dInd === "garbage") { - const items = deleteCourse(toolboxCourses, source.index); - setToolboxCourses(items); + if (sInd === "toolbox" && dInd === "toolbox") { + setToolboxCourses((prev) => + reorder(prev, source.index, destination.index) + ); + return; } - }; - const onDragStart = () => { - setIsDragging(true); + if (dInd === "garbage" && sInd === "toolbox") { + setToolboxCourses((prev) => { + const updated = [...prev]; + updated.splice(source.index, 1); + return updated; + }); + return; + } + + if (sInd === dInd && dInd.startsWith("planner-")) { + const semesterIndex = parseInt(dInd.split("-")[1]); + setPlannerCourses((prev) => + prev.map((semester, idx) => + idx === semesterIndex - 1 + ? { + ...semester, + courseList: reorder( + semester.courseList, + source.index, + destination.index + ), + } + : semester + ) + ); + return; + } + + if (sInd.startsWith("planner-") && dInd.startsWith("planner-")) { + const sourceSemesterIndex = parseInt(sInd.split("-")[1]); + const destSemesterIndex = parseInt(dInd.split("-")[1]); + + setPlannerCourses((prev) => { + const updated = [...prev]; + const sourceIdx = sourceSemesterIndex - 1; + const destIdx = destSemesterIndex - 1; + + const sourceSemester = updated[sourceIdx]; + const destSemester = updated[destIdx]; + + if ( + !sourceSemester || + !destSemester || + source.index < 0 || + source.index >= sourceSemester.courseList.length + ) { + console.warn("Invalid source or destination index"); + return prev; + } + + const courseListCopy = [...sourceSemester.courseList]; + const [movedCourse] = courseListCopy.splice(source.index, 1); + + if (!movedCourse) { + console.warn("Failed to extract course from source"); + return prev; + } + + updated[sourceIdx] = { + ...sourceSemester, + courseList: courseListCopy, + creditsTotal: + sourceSemester.creditsTotal - movedCourse.data.credit_max, + }; + + const destListCopy = [...destSemester.courseList]; + destListCopy.splice(destination.index, 0, movedCourse); + + updated[destIdx] = { + ...destSemester, + courseList: destListCopy, + creditsTotal: destSemester.creditsTotal + movedCourse.data.credit_max, + }; + + return updated; + }); + + return; + } + + const fromToolbox = sInd === "toolbox"; + const toPlanner = dInd.startsWith("planner-"); + + if (fromToolbox && toPlanner) { + const courseToClone = toolboxCourses.find((c) => c.name === draggableId); + const semesterIndex = parseInt(dInd.split("-")[1]); + + if (courseToClone) { + const newCourse = cloneCourse(courseToClone); + + setPlannerCourses((prev) => + prev.map((semester, idx) => { + if (idx + 1 === semesterIndex) { + const updatedList = [...semester.courseList]; + updatedList.splice(destination.index, 0, newCourse); + + return { + ...semester, + courseList: updatedList, + creditsTotal: + semester.creditsTotal + courseToClone.data.credit_max, + }; + } + return semester; + }) + ); + + setToolboxCourses((prev) => + prev + .map((c) => + c.name === courseToClone.name ? { ...c, count: c.count - 1 } : c + ) + .filter((c) => c.count > 0) + ); + } + + return; + } + + // if (sInd.startsWith("planner-") && dInd === "toolbox") { + // const sourceSemesterIndex = parseInt(sInd.split("-")[1]); + + // setPlannerCourses((prev) => { + // const updated = [...prev]; + // const sourceSemester = updated[sourceSemesterIndex - 1]; + // const courseListCopy = [...sourceSemester.courseList]; + // const [removedCourse] = courseListCopy.splice(source.index, 1); + + // if (!removedCourse) return prev; + + // updated[sourceSemesterIndex - 1] = { + // ...sourceSemester, + // courseList: courseListCopy, + // creditsTotal: + // sourceSemester.creditsTotal - removedCourse.data.credit_max, + // }; + + // setToolboxCourses((prevToolbox) => { + // const existing = prevToolbox.find( + // (c) => + // c.data.dept === removedCourse.data.dept && + // c.data.code_num === removedCourse.data.code_num + // ); + + // if (existing) { + // return prevToolbox.map((c) => + // c.name === existing.name ? { ...c, count: c.count + 1 } : c + // ); + // } else { + // return [ + // ...prevToolbox, + // { + // name: removedCourse.data.dept + removedCourse.data.code_num, + // data: removedCourse.data, + // count: 1, + // }, + // ]; + // } + // }); + + // return updated; + // }); + + // return; + // } + + if (sInd.startsWith("planner-") && dInd === "garbage") { + const sourceSemesterIndex = parseInt(sInd.split("-")[1]); + + setPlannerCourses((prev) => { + const updated = [...prev]; + const sourceSemester = updated[sourceSemesterIndex - 1]; + const courseListCopy = [...sourceSemester.courseList]; + const [removedCourse] = courseListCopy.splice(source.index, 1); + + if (!removedCourse) return prev; + + updated[sourceSemesterIndex - 1] = { + ...sourceSemester, + courseList: courseListCopy, + creditsTotal: + sourceSemester.creditsTotal - removedCourse.data.credit_max, + }; + + return updated; + }); + + return; + } }; return ( - <> -
-
-
- - - - - } - > - } /> - }> - - - - -
+
+
+
+ + + + + } + /> + + } + /> + + + +
- +
); } diff --git a/src/components/DraggableItem.tsx b/src/components/DraggableItem.tsx new file mode 100644 index 0000000..04399ab --- /dev/null +++ b/src/components/DraggableItem.tsx @@ -0,0 +1,53 @@ +import { Draggable } from "@hello-pangea/dnd"; +import PlannerCourse from "./PlannerComponents/PlannerCourse"; +import ToolboxCourse from "./Toolbox/ToolboxCourse"; +import { CourseType } from "../types/interfaces/Course.interface"; +interface DraggableItemProps { + name: string; + count: number; + index: number; + course: CourseType; + location: "toolbox" | "planner"; +} + +const DraggableItem: React.FC = ({ + name, + count, + index, + course, + location, +}) => { + return ( + + {(provided, snapshot) => ( +
+ {location === "planner" ? ( + + ) : ( + + )} +
+ )} +
+ ); +}; + +export default DraggableItem; diff --git a/src/components/PlannerComponents/AddSemester.tsx b/src/components/PlannerComponents/AddSemester.tsx new file mode 100644 index 0000000..52f180e --- /dev/null +++ b/src/components/PlannerComponents/AddSemester.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { SemesterType } from "../../types/interfaces/Semester.interface"; + +interface AddSemesterProps { + setPlannerCourses: React.Dispatch>; +} + +const AddSemester: React.FC = ({ setPlannerCourses }) => { + const handleAddCourse = () => { + setPlannerCourses((prevCourses) => [ + ...prevCourses, + { + semesterNumber: prevCourses.length + 1, + semesterSeason: "fall", + creditsTotal: 0, + courseList: [], + }, + ]); + }; + + return ( + <> + + + ); +}; + +export default AddSemester; diff --git a/src/components/PlannerComponents/PlannerCourse.tsx b/src/components/PlannerComponents/PlannerCourse.tsx index aa9cb06..7b49cd0 100644 --- a/src/components/PlannerComponents/PlannerCourse.tsx +++ b/src/components/PlannerComponents/PlannerCourse.tsx @@ -4,6 +4,8 @@ import { CourseType } from "../../types/interfaces/Course.interface"; interface PlannerCourseProps { course: CourseType; + isDragging: boolean; + index: number; } const PlannerCourse: React.FC = ({ course }) => { @@ -41,69 +43,59 @@ const PlannerCourse: React.FC = ({ course }) => { }; return ( - <> -
-
-
-
- -
- - {course.dept} - {course.code_num} - -

{toTitleCase(course.title)}

-
-
-
-
-

{course.credit_max}

-
- -
-
+
+
+
-
-

{course.credit_max}

+ +
+ + {course.dept} + {course.code_num} + +

{toTitleCase(course.title)}

-
- - {/* Popup Menu */} - {openPopup && ( +
-
- - -
- - -
+

{course.credit_max}

- )} + +
- + + {/* Popup Menu */} + {openPopup && ( +
+
+ + +
+ + +
+
+ )} +
); }; diff --git a/src/components/PlannerComponents/PlannerCourseHolder.tsx b/src/components/PlannerComponents/PlannerCourseHolder.tsx new file mode 100644 index 0000000..419e2a3 --- /dev/null +++ b/src/components/PlannerComponents/PlannerCourseHolder.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +interface PlannerCourseHolderProps { + isHover: boolean; +} + +const PlannerCourseHolder: React.FC = ({ + isHover, +}) => { + return ( + <> +
+

+ Try dragging a course from your Toolbox +

+ + {isHover && ( +
+

Drop it here!

+
+ )} +
+ + ); +}; + +export default PlannerCourseHolder; diff --git a/src/components/PlannerComponents/SemesterBlock.tsx b/src/components/PlannerComponents/SemesterBlock.tsx index b0d9b83..2344fb3 100644 --- a/src/components/PlannerComponents/SemesterBlock.tsx +++ b/src/components/PlannerComponents/SemesterBlock.tsx @@ -1,12 +1,15 @@ import React from "react"; -import PlannerCourse, { CourseType } from "../PlannerComponents/PlannerCourse"; import { SemesterType } from "../../types/interfaces/Semester.interface"; +import { Droppable } from "@hello-pangea/dnd"; +import PlannerCourseHolder from "./PlannerCourseHolder"; +import DraggableItem from "../DraggableItem"; interface SemesterBlockProps { semester: SemesterType; + index: number; } -const SemesterBlock: React.FC = ({ semester }) => { +const SemesterBlock: React.FC = ({ semester, index }) => { return ( <>
@@ -32,12 +35,39 @@ const SemesterBlock: React.FC = ({ semester }) => { {semester.creditsTotal}
+ + {(provided, snapshot) => { + const isEmpty = semester.courseList.length === 0; + const isHovering = snapshot.isDraggingOver; -
- {semester.courseList.map((course) => ( - - ))} -
+ return ( +
+ {isEmpty && !isHovering && ( + + )} + {isEmpty && isHovering && ( + + )} + {!isEmpty && + semester.courseList.map((course, index) => ( + + ))} + {provided.placeholder} +
+ ); + }} +

diff --git a/src/components/Toolbox/Toolbox.tsx b/src/components/Toolbox/Toolbox.tsx index fd35239..9518057 100644 --- a/src/components/Toolbox/Toolbox.tsx +++ b/src/components/Toolbox/Toolbox.tsx @@ -1,11 +1,11 @@ import React, { useState } from "react"; import ToolboxButton from "./ToolboxButton"; import NavButton from "./NavButton"; -import ToolboxCourse from "./ToolboxCourse"; import { Droppable } from "@hello-pangea/dnd"; import { IoIosArrowDown } from "react-icons/io"; import { CourseEntry } from "../../types/interfaces/Course.interface"; import GarbageBin from "../GarbageBin"; +import DraggableItem from "../DraggableItem"; interface ToolboxProps { courses: CourseEntry[]; @@ -56,12 +56,13 @@ const Toolbox: React.FC = ({ courses, isDragging }) => { className={`courses h-15 flex items-center px-2 w-screen overflow-x-auto whitespace-nowrap scrollbar-hide ${snapshot.isDraggingOver ? "bg-[#7e8eb4]" : ""}`} > {courses.map((course, index) => ( - ))} {provided.placeholder} diff --git a/src/components/Toolbox/ToolboxCourse.tsx b/src/components/Toolbox/ToolboxCourse.tsx index d97e4f3..c21e324 100644 --- a/src/components/Toolbox/ToolboxCourse.tsx +++ b/src/components/Toolbox/ToolboxCourse.tsx @@ -1,51 +1,31 @@ import React from "react"; -import { Draggable } from "@hello-pangea/dnd"; -// import type { -// DraggableProvided, -// DraggableStateSnapshot, -// } from "@hello-pangea/dnd"; interface ToolboxCourseProps { name: string; count: number; index: number; isDragging: boolean; - // provided: DraggableProvided; - // snapshot: DraggableStateSnapshot; } const ToolboxCourse: React.FC = ({ name, count, - index, isDragging, }) => { - const courseId = name.slice(0, 8); return ( - <> - - {(provided, snapshot) => { - return ( -
-
-

{count}

-
- {name} -
- ); - }} -
- +
+
+

{count}

+
+ {name} +
); }; diff --git a/src/pages/Planner.tsx b/src/pages/Planner.tsx index 0ab86fd..ff1a4bf 100644 --- a/src/pages/Planner.tsx +++ b/src/pages/Planner.tsx @@ -1,42 +1,38 @@ import React from "react"; -import PlannerCourse from "../components/PlannerComponents/PlannerCourse"; +import { Dispatch, SetStateAction } from "react"; import SemesterBlock from "../components/PlannerComponents/SemesterBlock"; +import { SemesterType } from "../types/interfaces/Semester.interface"; +import AddSemester from "../components/PlannerComponents/AddSemester"; -function Planner() { - const exampleCourse = { - dept: "CSCI", - code_num: "2600", - title: "PRINCIPLES OF SOFTWARE", - desc_text: - "A study of important concepts in software design, implementation, and testing. Topics include specification, abstraction with classes, design principles and patterns, testing, refactoring, the software development process, GUI and event-driven programming, and cloud-based programming. The course also introduces implementation and testing tools, including IDEs, revision control systems, and other frameworks. The overarching goal of the course is for students to learn how to write correct and maintainable software.", - credit_min: 4, - credit_max: 4, - sem_list: "Spring 2023,Spring 2024,Summer 2023,Summer 2024", - attr_list: "", - code_match: 0, - title_exact_match: 1, - title_start_match: 1, - title_match: 1, - title_acronym: 0, - title_abbrev: 1, - }; - - const exampleSemester = { - semesterNumber: 1, - semesterSeason: "FALL", - creditsTotal: 16, - courseList: [exampleCourse, exampleCourse, exampleCourse, exampleCourse], - }; +interface PlannerProps { + isDragging: boolean; + plannerCourses: SemesterType[]; + setPlannerCourses: Dispatch>; +} +const Planner: React.FC = ({ + isDragging, + plannerCourses, + setPlannerCourses, +}) => { return ( <> -
- {" "} - {" "} - {" "} +
+ {plannerCourses.map((semester, index) => { + return ( + + ); + })} +
); -} +}; export default Planner; diff --git a/src/types/interfaces/Semester.interface.ts b/src/types/interfaces/Semester.interface.ts index 981945f..1375f7e 100644 --- a/src/types/interfaces/Semester.interface.ts +++ b/src/types/interfaces/Semester.interface.ts @@ -1,8 +1,8 @@ -import { CourseType } from "../../components/PlannerComponents/PlannerCourse"; +import { CourseEntry } from "./Course.interface"; export interface SemesterType { semesterNumber: number; semesterSeason: string; creditsTotal: number; - courseList: CourseType[]; + courseList: CourseEntry[]; }