Skip to content
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
Binary file added .DS_Store
Binary file not shown.
458 changes: 336 additions & 122 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
"prettier:write": "prettier --write ."
},
"dependencies": {
"@hello-pangea/dnd": "^18.0.1",
"@tailwindcss/vite": "^4.0.6",
"axios": "^1.7.9",
"framer-motion": "^12.4.1",
"prettier": "^3.4.2",
"react": "^18.3.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.5",
Expand Down
112 changes: 93 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,105 @@
import React, { useState } from "react";
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 { Filters } from "./types/Filters";

function App() {
const [toolboxCourses, setToolboxCourses] = useState({});
// Original state from App.tsx
const [toolboxCourses, setToolboxCourses] = useState<CourseEntry[]>([]);
const [isDragging, setIsDragging] = useState<boolean>(false);

// Moved from Catalog.tsx
const [searchResults, setSearchResults] = useState<CourseType[]>([]);

// Moved from SearchBar.tsx
const [searchPrompt, setSearchPrompt] = useState("");
const [showFilter, setShowFilter] = useState(false);
const [filters, setFilters] = useState<Filters>({
Subject: [],
Attributes: [],
Semesters: []
});

const reorder = (
list: CourseEntry[],
startIndex: number,
endIndex: number
) => {
const result = Array.from(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);

return result;
};

const onDragEnd = (result: DropResult) => {
setIsDragging(false);
const { source, destination } = 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);
}
};

const onDragStart = () => {
setIsDragging(true);
};

return (
<>
<div className="font-['Helvetica'] bg-carpipink min-h-screen">
<Router>
<Routes>
<Route
path="/"
element={
<Catalog
toolboxCourses={toolboxCourses}
setToolboxCourses={setToolboxCourses}
/>
}
></Route>
<Route path="/filters" element={<DepartmentFilters />} />
<Route path="/planner" element={<Planner />}></Route>
</Routes>
<Toolbox courses={toolboxCourses} />
</Router>
<div className={`font-['Helvetica'] min-h-screen`}>
<div
className={`fixed top-0 left-0 w-full h-full z-0 bg-carpipink ${isDragging ? "brightness-50" : ""}`}
></div>
<div className={`relative z-10`}>
<DragDropContext onDragEnd={onDragEnd} onDragStart={onDragStart}>
<Router>
<Routes>
<Route
path="/"
element={
<Catalog
isDragging={isDragging}
toolboxCourses={toolboxCourses}
setToolboxCourses={setToolboxCourses}
searchResults={searchResults}
setSearchResults={setSearchResults}
searchPrompt={searchPrompt}
setSearchPrompt={setSearchPrompt}
showFilter={showFilter}
setShowFilter={setShowFilter}
filters={filters}
setFilters={setFilters}
/>
}
></Route>
<Route path="/filters" element={<DepartmentFilters />} />
<Route path="/planner" element={<Planner />}></Route>
</Routes>
<Toolbox courses={toolboxCourses} isDragging={isDragging} />
</Router>
</DragDropContext>
</div>
</div>
</>
);
Expand Down
41 changes: 28 additions & 13 deletions src/components/Course/Course.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ 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";
import {
CourseType,
CourseEntry,
} from "../../types/interfaces/Course.interface";

interface CourseProps {
course: CourseType;
toolboxCourses: { [key: string]: number };
setToolboxCourses: React.Dispatch<
React.SetStateAction<{ [key: string]: number }>
>;
toolboxCourses: CourseEntry[];
setToolboxCourses: React.Dispatch<React.SetStateAction<CourseEntry[]>>;
}

const Course: React.FC<CourseProps> = ({
Expand All @@ -29,18 +30,32 @@ const Course: React.FC<CourseProps> = ({
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
};

const courseDisplay = `${course.dept + course.code_num} ${toTitleCase(course.title)}`;
const courseDisplay: string = `${course.dept + course.code_num} ${toTitleCase(course.title)}`;
const addCourse = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setToolboxCourses((prevCourses) => ({
...prevCourses,
[courseDisplay]: (prevCourses[courseDisplay] || 0) + 1,
}));
};

const courseCount = toolboxCourses[courseDisplay];
setToolboxCourses((prevCourses) => {
const existingIndex = prevCourses.findIndex(
(c) => c.name === courseDisplay
);

if (existingIndex !== -1) {
return prevCourses.map((c, i) =>
i === existingIndex ? { ...c, count: c.count + 1 } : c
);
} else {
return [
...prevCourses,
{ name: courseDisplay, count: 1, data: course },
];
}
});
};
const toolboxCourse =
toolboxCourses[toolboxCourses.findIndex((c) => c.name === courseDisplay)];
const courseCount = toolboxCourse ? toolboxCourse.count : undefined;
return (
<>
<div
Expand Down
34 changes: 34 additions & 0 deletions src/components/GarbageBin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Droppable } from "@hello-pangea/dnd";
import React from "react";
import { MdDelete } from "react-icons/md";

interface GarbageBinProps {
isDragging: boolean;
}

const GarbageBin: React.FC<GarbageBinProps> = ({ isDragging }) => {
return (
<Droppable droppableId={`garbage`}>
{(provided, snapshot) => {
return (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={`absolute bottom-35 ${isDragging ? "" : "invisible"} w-fit`}
>
{provided.placeholder}
<div
className={` rounded-full border-2 m-4 p-4 border-red-600 text-5xl
transition-transform ease-in-out w-fit
${snapshot.isDraggingOver ? "bg-red-500 scale-115" : "bg-red-400"}`}
>
<MdDelete />
</div>
</div>
);
}}
</Droppable>
);
};

export default GarbageBin;
105 changes: 59 additions & 46 deletions src/components/PlannerComponents/PlannerCourse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,60 +38,73 @@ const PlannerCourse: React.FC<PlannerCourseProps> = ({ course }) => {
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
};

return (
<div className={`bg-[#283044] w-3/4 h-18 rounded-lg text-[#F5CECE] flex items-center relative`}>
<div className={`flex justify-between w-11/12 m-auto text-2xl`}>
<div className={`flex items-center`}>
<MdDragIndicator />
<div className={`text-sm ml-1`}>
<b>
{course.dept}
{course.code_num}
</b>
<p>{toTitleCase(course.title)}</p>
<>
<div className="flex flex-row">
<div
className={`bg-[#283044] w-full h-18 rounded-lg text-[#F5CECE] flex items-center`}
>
<div className={`flex justify-between w-11/12 m-auto text-2xl`}>
<div className={`flex items-center`}>
<MdDragIndicator />
<div className={`text-sm ml-1`}>
<b>
{course.dept}
{course.code_num}
</b>
<p>{toTitleCase(course.title)}</p>
</div>
</div>
<div className={`flex items-center`}>
<div
className={`rounded-full bg-[#F5CECE] text-[#283044] w-5 h-5 flex items-center justify-center text-sm mr-1 font-medium`}
>
<p>{course.credit_max}</p>
</div>
<MdOutlineMoreHoriz />
</div>
</div>
<div className={`flex items-center`}>
<div
className={`rounded-full bg-[#F5CECE] text-[#283044] w-5 h-5 flex items-center justify-center text-sm mr-1 font-medium`}
>
<p>{course.credit_max}</p>
</div>
<MdOutlineMoreHoriz
onClick={togglePopup}
className="cursor-pointer"
/>
</div>
</div>
<div className={`flex items-center`}>

{/* Popup Menu */}
{openPopup && (
<div
className={`rounded-full bg-[#F5CECE] text-[#283044] w-5 h-5 flex items-center justify-center text-sm mr-1 font-medium`}
ref={componentRef}
className="absolute bg-[#F5CECE] rounded-xl border border-slate-500 text-[#283044] text-xs p-2 right-0 top-8 shadow-lg"
>
<p>{course.credit_max}</p>
<div className="flex flex-col items-start space-y-1">
<button className="hover:bg-gray-300 p-1 w-full text-left">
Duplicate
</button>
<button className="hover:bg-gray-300 p-1 w-full text-left">
Move to next sem
</button>
<hr className="w-full border-gray-400" />
<button className="hover:bg-gray-300 p-1 w-full text-left">
Back to toolbox
</button>
<button className="hover:bg-red-300 p-1 w-full text-left">
Delete
</button>
</div>
</div>
<MdOutlineMoreHoriz
onClick={togglePopup}
className="cursor-pointer"
/>
</div>
)}
</div>


{/* Popup Menu */}
{openPopup && (
<div
ref={componentRef}
className="absolute bg-[#F5CECE] rounded-xl border border-slate-500 text-[#283044] text-xs p-2 right-0 top-8 shadow-lg"
>
<div className="flex flex-col items-start space-y-1">
<button className="hover:bg-gray-300 p-1 w-full text-left">
Duplicate
</button>
<button className="hover:bg-gray-300 p-1 w-full text-left">
Move to next sem
</button>
<hr className="w-full border-gray-400" />
<button className="hover:bg-gray-300 p-1 w-full text-left">
Back to toolbox
</button>
<button className="hover:bg-red-300 p-1 w-full text-left">
Delete
</button>
</div>
</div>
)}
</div>
</>
);
};

export default PlannerCourse;
export default PlannerCourse;
Loading