From 9d83d4dcab84f1d16924e2b7b669879fbf68219d Mon Sep 17 00:00:00 2001 From: August Fu Date: Tue, 9 Apr 2024 05:58:40 -0400 Subject: [PATCH 01/12] add dnd support to search panel, highlight searched rule, UI adjustments --- .../components/FourYearPlanPage.tsx | 11 ++-- .../components/Requirements/QObject.tsx | 56 ++++++++++++++++--- .../components/Requirements/ReqPanel.tsx | 12 +++- .../components/Requirements/Rule.tsx | 25 +++++++-- .../components/Search/CourseInSearch.tsx | 16 ++++-- .../components/Search/ResultsList.tsx | 2 +- .../components/Search/SearchPanel.tsx | 17 +++++- 7 files changed, 110 insertions(+), 29 deletions(-) diff --git a/frontend/degree-plan/components/FourYearPlanPage.tsx b/frontend/degree-plan/components/FourYearPlanPage.tsx index ad3b3eeb1..0f83f4ced 100644 --- a/frontend/degree-plan/components/FourYearPlanPage.tsx +++ b/frontend/degree-plan/components/FourYearPlanPage.tsx @@ -41,7 +41,7 @@ const PanelWrapper = styled(Pane)` height: 100%; display: flex; flex-direction: row; - gap: 2rem; + gap: 0.8rem; ` const PanelInteriorWrapper = styled.div<{ $maxWidth?: string; $minWidth?: string }>` @@ -104,6 +104,7 @@ const FourYearPlanPage = ({ const ref = useRef(null); // search panel + const [searchedRuleId, setSearchedRuleId] = useState(-1); const [searchPanelOpen, setSearchPanelOpen] = useState(false); const [searchRuleId, setSearchRuleId] = useState(null); const [searchRuleQuery, setSearchRuleQuery] = useState(null); // a query object @@ -160,7 +161,7 @@ const FourYearPlanPage = ({ // @ts-ignore */} {searchPanelOpen && ( - - + + )} diff --git a/frontend/degree-plan/components/Requirements/QObject.tsx b/frontend/degree-plan/components/Requirements/QObject.tsx index b5e2620d4..21b6592cd 100644 --- a/frontend/degree-plan/components/Requirements/QObject.tsx +++ b/frontend/degree-plan/components/Requirements/QObject.tsx @@ -72,7 +72,7 @@ const Attributes = ({ attributes }: { attributes: string[] }) => { } -const SearchConditionWrapper = styled(BaseCourseContainer)` +const SearchConditionWrapper = styled(BaseCourseContainer)<{$isSearched: boolean}>` display: flex; flex-wrap: wrap; gap: .5rem; @@ -80,6 +80,10 @@ const SearchConditionWrapper = styled(BaseCourseContainer)` box-shadow: 0px 0px 14px 2px rgba(0, 0, 0, 0.05); cursor: pointer; padding: .5rem .75rem; + ${props => !!props.$isSearched && ` + border-radius: 10px; + box-shadow: 8px 6px 10px 8px #00000026; + `} ` const Wrap = styled.span` @@ -174,15 +178,19 @@ interface SearchConditionProps extends SearchConditionInnerProps { ruleIsSatisfied: boolean, ruleId: Rule["id"]; ruleQuery: string; - activeDegreeplanId: DegreePlan["id"] + activeDegreeplanId: DegreePlan["id"]; + searchedRuleId: number; + setSearchedRuleId: (arg0: number) => void; } -const SearchCondition = ({ ruleId, ruleQuery, fulfillments, ruleIsSatisfied, q, activeDegreeplanId}: SearchConditionProps) => { +const SearchCondition = ({ ruleId, ruleQuery, fulfillments, ruleIsSatisfied, q, activeDegreeplanId, searchedRuleId, setSearchedRuleId}: SearchConditionProps) => { const { setSearchPanelOpen, setSearchRuleQuery, setSearchRuleId, setSearchFulfillments } = useContext(SearchPanelContext); return ( { @@ -194,7 +202,8 @@ const SearchCondition = ({ ruleId, ruleQuery, fulfillments, ruleIsSatisfied, q, setSearchRuleId(ruleId) } setSearchPanelOpen(true); - setSearchFulfillments(fulfillments) + setSearchFulfillments(fulfillments); + setSearchedRuleId(ruleId); }}> @@ -258,8 +267,10 @@ interface QObjectProps { rule: Rule; satisfied: boolean; activeDegreePlanId: number; + searchedRuleId: number; + setSearchedRuleId: (arg0: number) => void; } -const QObject = ({ q, fulfillments, rule, satisfied, activeDegreePlanId }: QObjectProps) => { +const QObject = ({ q, fulfillments, rule, satisfied, activeDegreePlanId, searchedRuleId, setSearchedRuleId }: QObjectProps) => { // recursively render switch (q.type) { @@ -298,7 +309,15 @@ const QObject = ({ q, fulfillments, rule, satisfied, activeDegreePlanId }: QObje const displaySearchConditions = searchConditions.map(search => { const courses = Array.from(fulfillmentsMap.values()) fulfillmentsMap.clear() - return + return }) return @@ -308,7 +327,16 @@ const QObject = ({ q, fulfillments, rule, satisfied, activeDegreePlanId }: QObje )} case "SEARCH": - return ; + return ; case "COURSE": const [fulfillment] = fulfillments.filter(fulfillment => fulfillment.full_code == q.full_code && (!q.semester || q.semester === fulfillment.semester)) return ; @@ -323,6 +351,8 @@ interface RuleLeafProps { rule: Rule; satisfied: boolean; activeDegreePlanId: number; + searchedRuleId: number; + setSearchedRuleId: (arg0: number) => void; } export const SkeletonRuleLeaf = () => ( @@ -343,7 +373,7 @@ export const SkeletonRuleLeaf = () => ( const RuleLeafWrapper = styled(Row)` margin-bottom: .5rem; ` -const RuleLeaf = ({ q_json, fulfillmentsForRule, rule, satisfied, activeDegreePlanId }: RuleLeafProps) => { +const RuleLeaf = ({ q_json, fulfillmentsForRule, rule, satisfied, activeDegreePlanId, searchedRuleId, setSearchedRuleId }: RuleLeafProps) => { const t1 = transformDepartmentInClauses(q_json); const t2 = transformCourseClauses(t1); const t3 = transformSearchConditions(t2) @@ -353,7 +383,15 @@ const RuleLeaf = ({ q_json, fulfillmentsForRule, rule, satisfied, activeDegreePl return ( - + ) } diff --git a/frontend/degree-plan/components/Requirements/ReqPanel.tsx b/frontend/degree-plan/components/Requirements/ReqPanel.tsx index 8ce3dd57d..0559531c4 100644 --- a/frontend/degree-plan/components/Requirements/ReqPanel.tsx +++ b/frontend/degree-plan/components/Requirements/ReqPanel.tsx @@ -162,7 +162,7 @@ const computeRuleTree = ({ activeDegreePlanId, rule, rulesToFulfillments }: Rule } -const Degree = ({degree, rulesToFulfillments, activeDegreeplan, editMode, setModalKey, setModalObject, isLoading}: any) => { +const Degree = ({degree, rulesToFulfillments, activeDegreeplan, editMode, setModalKey, setModalObject, isLoading, searchedRuleId, setSearchedRuleId}: any) => { const [collapsed, setCollapsed] = useState(false); if (isLoading) { return ( @@ -216,7 +216,9 @@ const Degree = ({degree, rulesToFulfillments, activeDegreeplan, editMode, setMod {degree && degree.rules.map((rule: any) => ( ))} } @@ -229,8 +231,10 @@ interface ReqPanelProps { setModalObject: (arg0: DegreePlan | null) => void; activeDegreeplan: DegreePlan | null; isLoading: boolean; + searchedRuleId: number; + setSearchedRuleId: (arg0: number) => void; } -const ReqPanel = ({setModalKey, setModalObject, activeDegreeplan, isLoading}: ReqPanelProps) => { +const ReqPanel = ({setModalKey, setModalObject, activeDegreeplan, isLoading, searchedRuleId, setSearchedRuleId}: ReqPanelProps) => { const [editMode, setEditMode] = React.useState(false); const { data: activeDegreeplanDetail = null, isLoading: isLoadingDegrees } = useSWR(activeDegreeplan ? `/api/degree/degreeplans/${activeDegreeplan.id}` : null); const { data: fulfillments, isLoading: isLoadingFulfillments } = useSWR(activeDegreeplan ? `/api/degree/degreeplans/${activeDegreeplan.id}/fulfillments` : null); @@ -271,6 +275,8 @@ const ReqPanel = ({setModalKey, setModalObject, activeDegreeplan, isLoading}: Re setModalKey={setModalKey} setModalObject={setModalObject} isLoading={isLoading} + searchedRuleId={searchedRuleId} + setSearchedRuleId={setSearchedRuleId} /> ))} {editMode && { diff --git a/frontend/degree-plan/components/Requirements/Rule.tsx b/frontend/degree-plan/components/Requirements/Rule.tsx index ca33a8f85..d38dc2b0f 100644 --- a/frontend/degree-plan/components/Requirements/Rule.tsx +++ b/frontend/degree-plan/components/Requirements/Rule.tsx @@ -100,7 +100,8 @@ const RuleLeafLabel = styled.div` ` const RuleLeafContainer = styled(Column)` - margin-top: 0.25rem; + margin: 0.25rem; + } ` @@ -146,7 +147,13 @@ export const SkeletonRule: React.FC = ({ children }) => /** * Recursive component to represent a rule. */ -const RuleComponent = (ruleTree : RuleTree) => { +interface RuleComponentProps { + ruleTree: RuleTree, + searchedRuleId: number, + setSearchedRuleId: (arg0: number) => void +} + +const RuleComponent = ({ruleTree, searchedRuleId, setSearchedRuleId} : RuleComponentProps) => { const { type, activeDegreePlanId, rule, progress } = ruleTree; const satisfied = progress === 1; @@ -180,7 +187,15 @@ const RuleComponent = (ruleTree : RuleTree) => { {rule.title} - + {!!satisfied && } @@ -211,7 +226,7 @@ const RuleComponent = (ruleTree : RuleTree) => { {children.map((ruleTree) => (
- +
))} @@ -240,7 +255,7 @@ const RuleComponent = (ruleTree : RuleTree) => { {children.map((ruleTree) => (
- +
))}
diff --git a/frontend/degree-plan/components/Search/CourseInSearch.tsx b/frontend/degree-plan/components/Search/CourseInSearch.tsx index 952d53f9e..7b5a3e499 100644 --- a/frontend/degree-plan/components/Search/CourseInSearch.tsx +++ b/frontend/degree-plan/components/Search/CourseInSearch.tsx @@ -9,6 +9,7 @@ import { Course as CourseType, DnDCourse, Rule } from "@/types"; import Skeleton from "react-loading-skeleton"; import 'react-loading-skeleton/dist/skeleton.css' import { ReviewPanelTrigger } from "../Infobox/ReviewPanel"; +import CourseComponent from "../Course/Course"; const RowSelectors = styled.li` @@ -155,6 +156,13 @@ export default function Course({ // isDragging: !!monitor.isDragging(), // }) // })) + const [{ isDragging }, drag] = useDrag(() => ({ + type: ItemTypes.COURSE_IN_PLAN, + item: {full_code: course.id}, + collect: (monitor) => ({ + isDragging: !!monitor.isDragging() + }) + }), [course]) const [isMouseOver, setIsMouseOver] = useState(false); @@ -166,12 +174,12 @@ export default function Course({ role="button" > - + {course.id.replace(/-/g, " ")} - {isMouseOver && + {/* {isMouseOver && - } +
} */} {course.title} @@ -188,4 +196,4 @@ export default function Course({ ); -} \ No newline at end of file +} diff --git a/frontend/degree-plan/components/Search/ResultsList.tsx b/frontend/degree-plan/components/Search/ResultsList.tsx index 94780c7bb..9bbbd24b8 100644 --- a/frontend/degree-plan/components/Search/ResultsList.tsx +++ b/frontend/degree-plan/components/Search/ResultsList.tsx @@ -93,7 +93,7 @@ const ResultsList = ({ // star means the course is a fulfillment isStar={!!fulfillments.find((fulfillment) => fulfillment.full_code == course.id)} />) : - Array.from(Array(3).keys()).map(() => ) + Array.from(Array(5).keys()).map(() => ) } diff --git a/frontend/degree-plan/components/Search/SearchPanel.tsx b/frontend/degree-plan/components/Search/SearchPanel.tsx index 0786f3de2..4027ca326 100644 --- a/frontend/degree-plan/components/Search/SearchPanel.tsx +++ b/frontend/degree-plan/components/Search/SearchPanel.tsx @@ -79,7 +79,12 @@ const SearchPanelHeader = styled(PanelHeader)` font-size: 1.25rem; ` -export const SearchPanel = ({ activeDegreeplanId }: { activeDegreeplanId: DegreePlan["id"] | null }) => { +interface SearchPanelProp { + activeDegreeplanId: DegreePlan["id"] | null; + setSearchedRuleId: (arg0: number) => void; +} + +export const SearchPanel = ({ activeDegreeplanId, setSearchedRuleId }: SearchPanelProp) => { const { setSearchPanelOpen, searchRuleId: ruleId, @@ -95,11 +100,17 @@ export const SearchPanel = ({ activeDegreeplanId }: { activeDegreeplanId: Degree setQueryString(""); }, [ruleId]) + const handleCloseSearch = () => { + setQueryString(""); + setSearchPanelOpen(false); + setSearchedRuleId(-1); + } + return ( Search - @@ -116,7 +127,7 @@ export const SearchPanel = ({ activeDegreeplanId }: { activeDegreeplanId: Degree value={queryString} onChange={(e) => {setQueryString(e.target.value)}} autoComplete="off" - placeholder={!ruleId ? "Search for a course!" : `Filtering for ${ruleQuery ? ruleQuery : 'a requirement'}`} + placeholder={!ruleId ? "Search for a course!" : `Filtering for a requirement`} /> From 1ceceb6da7cb1762663c79d7be583f5006a41699 Mon Sep 17 00:00:00 2001 From: August Fu Date: Tue, 9 Apr 2024 06:16:25 -0400 Subject: [PATCH 02/12] auto-resize plan panel when search panel is open --- frontend/degree-plan/components/FourYearPlanPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/degree-plan/components/FourYearPlanPage.tsx b/frontend/degree-plan/components/FourYearPlanPage.tsx index 0f83f4ced..638d88188 100644 --- a/frontend/degree-plan/components/FourYearPlanPage.tsx +++ b/frontend/degree-plan/components/FourYearPlanPage.tsx @@ -161,7 +161,10 @@ const FourYearPlanPage = ({ // @ts-ignore */} Date: Tue, 9 Apr 2024 12:37:59 -0400 Subject: [PATCH 03/12] make edit mode not turn off when you add or remove a semester --- frontend/degree-plan/components/FourYearPlan/Semesters.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/degree-plan/components/FourYearPlan/Semesters.tsx b/frontend/degree-plan/components/FourYearPlan/Semesters.tsx index 8e0be6c91..5db6cf311 100644 --- a/frontend/degree-plan/components/FourYearPlan/Semesters.tsx +++ b/frontend/degree-plan/components/FourYearPlan/Semesters.tsx @@ -247,7 +247,6 @@ const Semesters = ({ useEffect(() => { if (Object.keys(semesters).length == 0 && !isLoading) setEditMode(true); // if finish loading and no semesters, we go to edit mode for the user to add new semesters - else setEditMode(false); if (!activeDegreeplan) return; if (typeof window !== "undefined" && Object.keys(semesters).length) { localStorage.setItem( From 86540a0f2e4b14b09bc765f421e3b86aa7f5b3ee Mon Sep 17 00:00:00 2001 From: aadalal <57609353+AaDalal@users.noreply.github.com> Date: Tue, 9 Apr 2024 12:59:29 -0400 Subject: [PATCH 04/12] Make year a free input field when adding semesters --- .../components/FourYearPlan/Semesters.tsx | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/frontend/degree-plan/components/FourYearPlan/Semesters.tsx b/frontend/degree-plan/components/FourYearPlan/Semesters.tsx index 5db6cf311..8410d3e67 100644 --- a/frontend/degree-plan/components/FourYearPlan/Semesters.tsx +++ b/frontend/degree-plan/components/FourYearPlan/Semesters.tsx @@ -66,10 +66,31 @@ const AddButton = styled.div` gap: 1rem; `; +const YearInput = styled.input` + width: 9rem; + background-color: transparent; + border-color: #9FB5EF; + color: #C1C1C1; + box-shadow: none; + &:hover { + borderColor: "#9FB5EF"; + } + + padding: .75rem; + padding-top: .5rem; + padding-bottom: .5rem; + border-style: solid; + border-radius: .25rem; + border-width: 1px; + border-top-left-radius: 0; + border-top-right-radius: 0; + font-size: 1rem; +` + const selectStyles = (topOrBottom: boolean) => ({ control: (provided: any) => ({ ...provided, - width: "130px", + width: "9rem", backgroundColor: "transparent", borderColor: "#9FB5EF", color: "#C1C1C1", @@ -77,11 +98,9 @@ const selectStyles = (topOrBottom: boolean) => ({ "&:hover": { borderColor: "#9FB5EF", }, - ...( - topOrBottom ? - { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, borderBottom: 0 } : - { borderTopLeftRadius: 0, borderTopRightRadius: 0 } - ) + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + borderBottom: 0 }), singleValue: (provided: any) => ({ ...provided, @@ -126,14 +145,6 @@ const ModifySemesters = ({ { value: "C", label: "Fall" }, ]; - // TODO: Un-hardcode years - const yearOptions = [ - { value: "2024", label: "2024" }, - { value: "2025", label: "2025" }, - { value: "2026", label: "2026" }, - { value: "2027", label: "2027" }, - ]; - return ( // TODO: add a modal for this @@ -153,11 +164,10 @@ const ModifySemesters = ({ onChange={(option) => setSelectedSeason(option ? option.value : selectedSeason)} /> - option.value === selectedSeason)} - onChange={(option) => setSelectedSeason(option ? option.value : selectedSeason)} - /> - - setSelectedYear(e.target.value)} - /> - + + + option.value === selectedSeason)} - onChange={(option) => setSelectedSeason(option ? option.value : selectedSeason)} - /> - setSelectedYear(e.target.value)} - /> - - - Cancel - - Add - - - + // TODO: add a modal for this + + + + + + +
Add Semester
+
+
+ + option.value === selectedYear)} + onChange={(option) => setSelectedYear(option ? option.value : selectedYear)} + /> +
); }; @@ -200,8 +182,6 @@ interface SemestersProps { setEditMode: (arg0: boolean) => void; isLoading: boolean; currentSemester?: string; - showAddSemModal: boolean; - setShowAddSemModal: (arg0: boolean) => void; } const Semesters = ({ @@ -212,8 +192,6 @@ const Semesters = ({ setModalKey, setModalObject, setEditMode, - showAddSemModal, - setShowAddSemModal, currentSemester, isLoading, }: SemestersProps) => { @@ -236,7 +214,6 @@ const Semesters = ({ }>({}); const addSemester = (semester: string) => { if (!semesters[semester]) setSemesters({ ...semesters, [semester]: [] }); - setShowAddSemModal(false); }; const removeSemester = (semester: string) => { @@ -270,6 +247,7 @@ const Semesters = ({ useEffect(() => { if (Object.keys(semesters).length == 0 && !isLoading) setEditMode(true); // if finish loading and no semesters, we go to edit mode for the user to add new semesters + else setEditMode(false); if (!activeDegreeplan) return; if (typeof window !== "undefined" && Object.keys(semesters).length) { localStorage.setItem( @@ -320,16 +298,8 @@ const Semesters = ({ currentSemester={currentSemester} /> ))} - {showAddSemModal && ( - {setModalKey(null); setShowAddSemModal(false);}} - modalKey={'semester-add'} - > - {/* - // @ts-ignore */} - - + {editMode && ( + )} ); diff --git a/frontend/degree-plan/styles/globals.css b/frontend/degree-plan/styles/globals.css index 76ef6a9f5..96626cde8 100644 --- a/frontend/degree-plan/styles/globals.css +++ b/frontend/degree-plan/styles/globals.css @@ -65,8 +65,6 @@ --callout-border-rgb: 172, 175, 176; --card-rgb: 180, 185, 188; --card-border-rgb: 131, 134, 135; - - font-size: 14px; } /*