diff --git a/.vite/deps/_metadata.json b/.vite/deps/_metadata.json new file mode 100644 index 0000000..a29e26b --- /dev/null +++ b/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "625b799d", + "configHash": "a9b0ffca", + "lockfileHash": "a6ff8f94", + "browserHash": "6b8947ac", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/.vite/deps/package.json b/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/package-lock.json b/package-lock.json index 51745e1..7951d41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.0.2", + "framer-motion": "^12.4.1", "prettier": "^3.4.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.4.0", "react-router-dom": "^7.1.5", "tailwindcss": "^4.0.2" }, @@ -2487,6 +2489,33 @@ "dev": true, "license": "ISC" }, + "node_modules/framer-motion": { + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.1.tgz", + "integrity": "sha512-5Ijbea3topSZjadQ0hgc/TcWj2ldMZmNREM7RvAhvsThYOA1HHOA8TT1yKvMu1YXP3jWaFwoZ6Vo9Nw+DUZrzA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.0.0", + "motion-utils": "^12.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3046,6 +3075,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz", + "integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.0.0" + } + }, + "node_modules/motion-utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz", + "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3296,6 +3340,15 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", + "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -3553,6 +3606,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/turbo-stream": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", diff --git a/package.json b/package.json index 4a6636f..6afe8be 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ }, "dependencies": { "@tailwindcss/vite": "^4.0.2", + "framer-motion": "^12.4.1", "prettier": "^3.4.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.4.0", "react-router-dom": "^7.1.5", "tailwindcss": "^4.0.2" }, diff --git a/src/App.tsx b/src/App.tsx index 5cc2d69..88eaf92 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,16 +2,27 @@ import React, { useState } from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import Catalog from "./pages/Catalog"; import Planner from "./pages/Planner"; +import Toolbox from "./components/Toolbox/Toolbox"; function App() { + const [toolboxCourses, setToolboxCourses] = useState({}); return ( <> -
+
- }> + + } + > }> +
diff --git a/src/components/Course.tsx b/src/components/Course.tsx deleted file mode 100644 index c0ac2a8..0000000 --- a/src/components/Course.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -function Course() { - return <>; -} - -export default Course; diff --git a/src/components/Course/AddButton.tsx b/src/components/Course/AddButton.tsx new file mode 100644 index 0000000..3317048 --- /dev/null +++ b/src/components/Course/AddButton.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { IoAdd } from "react-icons/io5"; + +interface AddButtonProps { + addCourse: (e: React.MouseEvent) => void; +} +const AddButton: React.FC = ({ addCourse }) => { + return ( + <> + + + ); +}; + +export default AddButton; diff --git a/src/components/Course/Course.tsx b/src/components/Course/Course.tsx new file mode 100644 index 0000000..1d7fd43 --- /dev/null +++ b/src/components/Course/Course.tsx @@ -0,0 +1,85 @@ +import React, { useState } from "react"; +import Tag from "./Tag"; +import AddButton from "./AddButton"; +import { motion } from "framer-motion"; +import { CourseType } from "../../types/interfaces/Course.interface"; + +interface CourseProps { + course: CourseType; + toolboxCourses: { [key: string]: number }; + setToolboxCourses: React.Dispatch< + React.SetStateAction<{ [key: string]: number }> + >; +} + +const Course: React.FC = ({ + course, + toolboxCourses, + setToolboxCourses, +}) => { + const [isOpen, setIsOpen] = useState(false); + const toggleOpen = (e: React.MouseEvent) => { + const target = e.target as HTMLElement; + if (target.id !== "add-button") setIsOpen((open) => !open); + }; + + const courseDisplay = `${course.department + course.code} ${course.name}`; + const addCourse = (e: React.MouseEvent) => { + e.stopPropagation(); + setToolboxCourses((prevCourses) => ({ + ...prevCourses, + [courseDisplay]: (prevCourses[courseDisplay] || 0) + 1, + })); + }; + + const courseCount = toolboxCourses[courseDisplay]; + return ( + <> +
+
+

{courseCount}

+
+
+
+

+ {course.department} + {course.code} {course.name} +

+
+ + {course.attributesList.map((attr) => { + return ; + })} + {course.semestersOffered.map((semester) => { + return ; + })} +
+
+
+ +
+
+
+ + {course.description} + +
+
+ + ); +}; + +export default Course; diff --git a/src/components/Course/Tag.tsx b/src/components/Course/Tag.tsx new file mode 100644 index 0000000..f5c848a --- /dev/null +++ b/src/components/Course/Tag.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +interface TagProp { + name: string; + color: string; +} +const Tag: React.FC = ({ name, color }) => { + return ( + <> +
+ {name} +
+ + ); +}; + +export default Tag; diff --git a/src/components/PlannerComponents/PlannerCourse.tsx b/src/components/PlannerComponents/PlannerCourse.tsx new file mode 100644 index 0000000..55f944e --- /dev/null +++ b/src/components/PlannerComponents/PlannerCourse.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { MdDragIndicator, MdOutlineMoreHoriz } from "react-icons/md"; +import { CourseType } from "../../types/interfaces/Course.interface"; + +interface PlannerCourseProps { + course: CourseType; +} + +const PlannerCourse: React.FC = ({ course }) => { + return ( + <> +
+
+
+ +
+ + {course.department} + {course.code} + +

{course.name}

+
+
+
+
+

{course.credits}

+
+ +
+
+
+ + ); +}; + +export default PlannerCourse; diff --git a/src/components/Toolbox/NavButton.tsx b/src/components/Toolbox/NavButton.tsx new file mode 100644 index 0000000..193b22d --- /dev/null +++ b/src/components/Toolbox/NavButton.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { Link, useLocation } from "react-router-dom"; + +const NavButton = () => { + const location = useLocation(); + const path = location.pathname; + + return ( + <> + + {path == "/" ? "Go to Planner" : "Go to Courses"} + + + ); +}; + +export default NavButton; diff --git a/src/components/Toolbox/Toolbox.tsx b/src/components/Toolbox/Toolbox.tsx new file mode 100644 index 0000000..0d64468 --- /dev/null +++ b/src/components/Toolbox/Toolbox.tsx @@ -0,0 +1,60 @@ +import React, { useState } from "react"; +import ToolboxButton from "./ToolboxButton"; +import NavButton from "./NavButton"; +import ToolboxCourse from "./ToolboxCourse"; + +import { IoIosArrowDown } from "react-icons/io"; + +interface ToolboxProps { + courses: { [key: string]: number }; +} + +const Toolbox: React.FC = ({ courses }) => { + const [isOpen, setIsOpen] = useState(false); + + const toggleToolbox = () => { + setIsOpen((open) => !open); + }; + + const toolboxSum = () => { + let sum = 0; + Object.values(courses).forEach((value) => (sum += value)); + return sum; + }; + return ( + <> +
+ +
+
+
+ +
+
+ {Object.entries(courses).map(([key, value]) => ( + + ))} +
+
+ +
+
+ + ); +}; + +export default Toolbox; diff --git a/src/components/Toolbox/ToolboxButton.tsx b/src/components/Toolbox/ToolboxButton.tsx new file mode 100644 index 0000000..3b7dc09 --- /dev/null +++ b/src/components/Toolbox/ToolboxButton.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { PiToolbox } from "react-icons/pi"; + +interface ToolboxButtonProps { + toggleToolbox: (e: React.MouseEvent) => void; + isOpen: boolean; + count: number; +} +const ToolboxButton: React.FC = ({ + toggleToolbox, + isOpen, + count, +}) => { + return ( + <> + + + ); +}; + +export default ToolboxButton; diff --git a/src/components/Toolbox/ToolboxCourse.tsx b/src/components/Toolbox/ToolboxCourse.tsx new file mode 100644 index 0000000..e010df5 --- /dev/null +++ b/src/components/Toolbox/ToolboxCourse.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +interface ToolboxCourseProps { + name: string; + count: number; +} + +const ToolboxCourse: React.FC = ({ name, count }) => { + return ( + <> +
+
+

{count}

+
+ {name} +
+ + ); +}; + +export default ToolboxCourse; diff --git a/src/pages/Catalog.tsx b/src/pages/Catalog.tsx index 9e27a56..4c53cfc 100644 --- a/src/pages/Catalog.tsx +++ b/src/pages/Catalog.tsx @@ -1,11 +1,54 @@ import React from "react"; +import Course from "../components/Course/Course"; -function Catalog() { +interface CatalogProps { + toolboxCourses: { [key: string]: number }; + setToolboxCourses: React.Dispatch< + React.SetStateAction<{ [key: string]: number }> + >; +} + +const Catalog: React.FC = ({ + toolboxCourses, + setToolboxCourses, +}) => { + const exampleCourse = { + id: 1, + name: "Principles of Software", + department: "CSCI", + code: "2600", + description: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", + attributesList: ["Commuication Intensive", "HASS"], + semestersOffered: ["Fall", "Spring"], + credits: 4, + }; + + const secondExample = { + id: 1, + name: "Introduction to Algorithms", + department: "CSCI", + code: "2300", + description: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", + attributesList: ["Commuication Intensive", "HASS"], + semestersOffered: ["Fall", "Spring"], + credits: 4, + }; return ( <> -

This is the catalog

+ + ); -} +}; export default Catalog; diff --git a/src/pages/Planner.tsx b/src/pages/Planner.tsx index 93ff837..9cecf83 100644 --- a/src/pages/Planner.tsx +++ b/src/pages/Planner.tsx @@ -1,9 +1,21 @@ import React from "react"; +import PlannerCourse from "../components/PlannerComponents/PlannerCourse"; function Planner() { + const exampleCourse = { + id: 1, + name: "Principles of Software", + department: "CSCI", + code: "2600", + description: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", + attributesList: ["Commuication Intensive", "HASS"], + semestersOffered: ["Fall", "Spring"], + credits: 4, + }; return ( <> -

This is the planners

+ ); } diff --git a/src/types/interfaces/Course.interface.ts b/src/types/interfaces/Course.interface.ts new file mode 100644 index 0000000..4729ef2 --- /dev/null +++ b/src/types/interfaces/Course.interface.ts @@ -0,0 +1,10 @@ +export interface CourseType { + id: number; + name: string; + department: string; + code: string; + description: string; + attributesList: string[]; + semestersOffered: string[]; + credits: number; +}