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

Coursebag #480

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 5 additions & 2 deletions api/src/controllers/roadmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ router.post('/', async function (req: Request<never, unknown, Record<string, unk
}
console.log(`Adding Roadmap: ${JSON.stringify(req.body)}`);
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({});
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={showCourseBag ? { color: 'var(--primary)' } : { color: 'var(--text-color)' }} />
</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 @@ -12,6 +12,9 @@ import ThemeContext from '../../style/theme-context';
interface CourseProps extends CourseGQLData {
requiredCourses?: string[];
onDelete?: () => void;
onAddToBag?: () => void;
isInBag?: boolean;
removeFromBag?: () => void;
}

const Course: FC<CourseProps> = (props) => {
Expand All @@ -28,6 +31,9 @@ const Course: FC<CourseProps> = (props) => {
requiredCourses,
terms,
onDelete,
onAddToBag,
isInBag,
removeFromBag,
} = props;

const CoursePopover = (
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;
}
16 changes: 11 additions & 5 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,11 +58,16 @@ const Planner: FC = () => {
planners: collapseAllPlanners(allPlanData),
transfers: transfers,
};
const coursebagStrings = coursebag.map((course) => course.id);
let savedAccount = false;
// if logged in
if (cookies.user !== undefined) {
// save data to account
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);
savedAccount = true;
}
Expand Down Expand Up @@ -112,16 +119,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
Loading