Skip to content

Commit

Permalink
Coursebag (#480)
Browse files Browse the repository at this point in the history
* coursbeag on sidebag

* added coursebag to catalog

* close tag

* lint

* pnpm lock

* checked pnpm lock

* space added

* chore: fixed unnecessary changes
  • Loading branch information
timobraz authored Oct 16, 2024
1 parent f5fda8a commit 5bd1207
Show file tree
Hide file tree
Showing 16 changed files with 314 additions and 85 deletions.
8 changes: 6 additions & 2 deletions api/src/controllers/roadmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ router.post('/', async function (req: Request<never, unknown, Record<string, unk
return;
}
console.log(`Adding Roadmap: ${JSON.stringify(req.body)}`);

try {
if (await Roadmap.exists({ userID: req.body._id })) {
await Roadmap.replaceOne({ userID: req.body._id }, { roadmap: req.body.roadmap, userID: req.body._id });
await Roadmap.replaceOne(
{ userID: req.body._id },
{ roadmap: req.body.roadmap, userID: req.body._id, coursebag: req.body.coursebag },
);
} else {
// add roadmap to mongo
await new Roadmap({ roadmap: req.body.roadmap, userID: req.body._id }).save();
await new Roadmap({ roadmap: req.body.roadmap, userID: req.body._id, coursebag: req.body.coursebag }).save();
}
res.json({});
} catch {
Expand Down
4 changes: 4 additions & 0 deletions api/src/models/roadmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const roadmapSchema = new mongoose.Schema({
type: String,
required: true,
},
coursebag: {
type: [String],
required: true,
},
});

const Roadmap = mongoose.model('Roadmap', roadmapSchema);
Expand Down
14 changes: 13 additions & 1 deletion site/src/component/SearchModule/SearchModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import './SearchModule.scss';
import wfs from 'websoc-fuzzy-search';
import Form from 'react-bootstrap/Form';
import InputGroup from 'react-bootstrap/InputGroup';
import { Search } from 'react-bootstrap-icons';
import { Bag, Search } from 'react-bootstrap-icons';

import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { setHasFullResults, setLastQuery, setNames, setPageNumber, setResults } from '../../store/slices/searchSlice';
import { searchAPIResults } from '../../helpers/util';
import { SearchIndex } from '../../types/types';
import { NUM_RESULTS_PER_PAGE } from '../../helpers/constants';
import { setShowCourseBag } from '../../store/slices/roadmapSlice';

const SEARCH_TIMEOUT_MS = 300;
const FULL_RESULT_THRESHOLD = 3;
Expand All @@ -22,6 +23,7 @@ interface SearchModuleProps {
const SearchModule: FC<SearchModuleProps> = ({ index }) => {
const dispatch = useAppDispatch();
const search = useAppSelector((state) => state.search[index]);
const showCourseBag = useAppSelector((state) => state.roadmap.showCourseBag);
const [pendingRequest, setPendingRequest] = useState<number | null>(null);
const [prevIndex, setPrevIndex] = useState<SearchIndex | null>(null);

Expand Down Expand Up @@ -135,6 +137,16 @@ const SearchModule: FC<SearchModuleProps> = ({ index }) => {
placeholder={placeholder}
onChange={(e) => searchNamesAfterTimeout(e.target.value)}
/>
{
// only show course bag icon on roadmap page
location.pathname === '/roadmap' && (
<InputGroup.Append>
<InputGroup.Text onClick={() => dispatch(setShowCourseBag(!showCourseBag))}>
<Bag style={{ color: showCourseBag ? 'var(--primary)' : 'var(--text-color)', cursor: 'pointer' }} />
</InputGroup.Text>
</InputGroup.Append>
)
}
</InputGroup>
</Form.Group>
</div>
Expand Down
12 changes: 9 additions & 3 deletions site/src/helpers/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { searchAPIResults } from './util';
import { RoadmapPlan, defaultPlan } from '../store/slices/roadmapSlice';
import {
BatchCourseData,
Coursebag,
InvalidCourseData,
MongoRoadmap,
PlannerData,
Expand Down Expand Up @@ -124,9 +125,10 @@ interface RoadmapCookies {

export const loadRoadmap = async (
cookies: RoadmapCookies,
loadHandler: (r: RoadmapPlan[], s: SavedRoadmap, isLocalNewer: boolean) => void,
loadHandler: (r: RoadmapPlan[], s: SavedRoadmap, coursebag: Coursebag, isLocalNewer: boolean) => void,
) => {
let roadmap: SavedRoadmap = null!;
let coursebagStrings: string[] = [];
const localRoadmap: SavedRoadmap = JSON.parse(localStorage.getItem('roadmap') ?? 'null');
// if logged in
if (cookies.user !== undefined) {
Expand All @@ -136,6 +138,9 @@ export const loadRoadmap = async (
if (request.data.roadmap !== undefined) {
roadmap = request.data.roadmap;
}
if (request.data.coursebag !== undefined) {
coursebagStrings = request.data.coursebag;
}
}

let isLocalNewer = false;
Expand All @@ -154,10 +159,11 @@ export const loadRoadmap = async (
'planners' in roadmap
? roadmap.planners
: [{ name: defaultPlan.name, content: (roadmap as { planner: SavedPlannerYearData[] }).planner }];

// expand planner and set the state
const planners = await expandAllPlanners(loadedData);
loadHandler(planners, roadmap, isLocalNewer);
const coursesObj: BatchCourseData = (await searchAPIResults('courses', coursebagStrings)) as BatchCourseData;
const coursebag = coursebagStrings.map((id) => coursesObj[id]);
loadHandler(planners, roadmap, coursebag, isLocalNewer);
};

type PrerequisiteNode = Prerequisite | PrerequisiteTree;
Expand Down
14 changes: 12 additions & 2 deletions site/src/pages/RoadmapPage/Course.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC } from 'react';
import './Course.scss';
import { Button } from 'react-bootstrap';
import { InfoCircle, ExclamationTriangle, Trash } from 'react-bootstrap-icons';
import { InfoCircle, ExclamationTriangle, Trash, BagPlus, BagFill } from 'react-bootstrap-icons';
import CourseQuarterIndicator from '../../component/QuarterTooltip/CourseQuarterIndicator';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
Expand All @@ -13,6 +13,9 @@ interface CourseProps extends CourseGQLData {
requiredCourses?: string[];
unmatchedPrerequisites?: string[];
onDelete?: () => void;
onAddToBag?: () => void;
isInBag?: boolean;
removeFromBag?: () => void;
}

const Course: FC<CourseProps> = (props) => {
Expand All @@ -29,6 +32,9 @@ const Course: FC<CourseProps> = (props) => {
requiredCourses,
terms,
onDelete,
onAddToBag,
isInBag,
removeFromBag,
} = props;
const CoursePopover = (
<Popover id={'course-popover-' + id}>
Expand Down Expand Up @@ -107,14 +113,18 @@ const Course: FC<CourseProps> = (props) => {
)}
</div>
<div className="title">{title}</div>
{/* <div className="course-footer">
<div className="course-footer">
{onAddToBag && !isInBag && <BagPlus onClick={onAddToBag}></BagPlus>}
{isInBag && <BagFill onClick={removeFromBag}></BagFill>}
{/* <div className="course-footer">
{requiredCourses && (
<OverlayTrigger trigger={['hover', 'focus']} placement="right" overlay={WarningPopover} delay={100}>
<ExclamationTriangle />
</OverlayTrigger>
)}
{/* <div className="units">{minUnits === maxUnits ? minUnits : `${minUnits}-${maxUnits}`} units</div> * /}
</div> */}
</div>
</div>
);
};
Expand Down
45 changes: 45 additions & 0 deletions site/src/pages/RoadmapPage/CourseBag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { removeCourseFromBag } from '../../store/slices/roadmapSlice';
import Course from './Course';
import './Coursebag.scss';
import { Draggable } from 'react-beautiful-dnd';
const CourseBag = () => {
const { coursebag } = useAppSelector((state) => state.roadmap);
const dispatch = useAppDispatch();

return (
<div className="coursebag-container">
<h3 className="coursebag-title">Course Bag</h3>
<div style={{ height: '100%' }}>
{coursebag.map((course, index) => {
return (
<Draggable draggableId={`coursebag-${course.id}-${index}`} key={`coursebag-${index}`} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
// use inline style here so dnd can calculate size
margin: ' 0rem 2rem 1rem 2rem',
cursor: 'grab',
...provided.draggableProps.style,
}}
>
<Course
{...course}
onDelete={() => {
dispatch(removeCourseFromBag(course));
}}
/>
</div>
)}
</Draggable>
);
})}
</div>
</div>
);
};

export default CourseBag;
23 changes: 20 additions & 3 deletions site/src/pages/RoadmapPage/CourseHitItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { FC } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { useAppDispatch } from '../../store/hooks';
import { setActiveCourse, setShowAddCourse } from '../../store/slices/roadmapSlice';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import {
addCourseToBag,
removeCourseFromBag,
setActiveCourse,
setShowAddCourse,
} from '../../store/slices/roadmapSlice';
import Course from './Course';

import { useIsMobile } from '../../helpers/util';
Expand All @@ -14,6 +19,8 @@ interface CourseHitItemProps extends CourseGQLData {
const CourseHitItem: FC<CourseHitItemProps> = (props: CourseHitItemProps) => {
const dispatch = useAppDispatch();
const isMobile = useIsMobile();
const coursebag = useAppSelector((state) => state.roadmap.coursebag);
const isInBag = coursebag.some((course) => course.id === props.id);
// do not make course draggable on mobile
const onMobileMouseDown = () => {
dispatch(setActiveCourse(props));
Expand All @@ -25,6 +32,16 @@ const CourseHitItem: FC<CourseHitItemProps> = (props: CourseHitItemProps) => {
onMobileMouseDown();
}
};
const onAddToBag = () => {
if (!props) return;
if (props.id === undefined) return;
if (coursebag.some((course) => course.id === props.id)) return;
dispatch(addCourseToBag(props));
};
const removeFromBag = () => {
dispatch(removeCourseFromBag(props));
};

if (isMobile) {
return (
<div
Expand Down Expand Up @@ -60,7 +77,7 @@ const CourseHitItem: FC<CourseHitItemProps> = (props: CourseHitItemProps) => {
...provided.draggableProps.style,
}}
>
<Course {...props} />
<Course {...props} onAddToBag={onAddToBag} isInBag={isInBag} removeFromBag={removeFromBag} />
</div>
);
}}
Expand Down
29 changes: 29 additions & 0 deletions site/src/pages/RoadmapPage/Coursebag.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.coursebag-container {
padding-top: 2vh;
overflow-y: auto;
height: 100%;
}
.coursebag-title {
font-size: 2rem;
font-weight: 500;
padding: 0 1.8rem;
}
.no-results {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
font-size: 1.5rem;
padding: 2rem;
text-align: center;

img {
width: 400px;
max-width: 100%;
}
}

.search-pagination {
display: flex;
justify-content: center;
}
13 changes: 7 additions & 6 deletions site/src/pages/RoadmapPage/Planner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
selectAllPlans,
setAllPlans,
defaultPlan,
setCoursebag,
} from '../../store/slices/roadmapSlice';
import { useFirstRender } from '../../hooks/firstRenderer';
import { SavedRoadmap, MongoRoadmap } from '../../types/types';
Expand All @@ -29,6 +30,7 @@ const Planner: FC = () => {
const currentPlanData = useAppSelector(selectYearPlans);
const allPlanData = useAppSelector(selectAllPlans);
const transfers = useAppSelector((state) => state.roadmap.transfers);
const coursebag = useAppSelector((state) => state.roadmap.coursebag);
const [showSyncModal, setShowSyncModal] = useState(false);

const [missingPrerequisites, setMissingPrerequisites] = useState(new Set<string>());
Expand Down Expand Up @@ -56,16 +58,16 @@ const Planner: FC = () => {
planners: collapseAllPlanners(allPlanData),
transfers: transfers,
};
const coursebagStrings = coursebag.map((course) => course.id);

// save to local storage as well
localStorage.setItem('roadmap', JSON.stringify(roadmap));

// mark changes as saved to bypass alert on page leave
dispatch(setUnsavedChanges(false));

// if logged in, save data to account
if (cookies.user !== undefined) {
const mongoRoadmap: MongoRoadmap = { _id: cookies.user.id, roadmap: roadmap };
const mongoRoadmap: MongoRoadmap = { _id: cookies.user.id, roadmap: roadmap, coursebag: coursebagStrings };
axios.post('/api/roadmap', mongoRoadmap).then((res) => {
// error saving to account, saved locally
if (res.data.error) {
Expand Down Expand Up @@ -113,16 +115,15 @@ const Planner: FC = () => {

// if first render and current roadmap is empty, load from local storage
if (isFirstRenderer && roadmapStr === emptyRoadmap) {
loadRoadmap(cookies, (planners, roadmap, isLocalNewer) => {
loadRoadmap(cookies, (planners, roadmap, coursebag, isLocalNewer) => {
dispatch(setAllPlans(planners));
dispatch(setTransfers(roadmap.transfers));
dispatch(setCoursebag(coursebag));
if (isLocalNewer) {
setShowSyncModal(true);
}
});
}
// validate planner every time something changes
else {
} else {
validatePlanner(transfers, currentPlanData, (missing, invalid) => {
// set missing courses
setMissingPrerequisites(missing);
Expand Down
Loading

0 comments on commit 5bd1207

Please sign in to comment.