Skip to content

Commit e126d51

Browse files
spotta85Copilot
andcommitted
basic course add panel structure.
Co-authored-by: Copilot <copilot@github.com>
1 parent 3405e08 commit e126d51

2 files changed

Lines changed: 143 additions & 2 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { CourseSearchContent } from "@/entrypoints/components/course-add-modal";
2+
import { searchCatalogCourses } from "@/lib/backend/db";
3+
import type { CatalogCourse } from "@/lib/general-types";
4+
import {
5+
CaretLeftIcon,
6+
DotsSixVerticalIcon,
7+
PlusCircleIcon,
8+
PlusIcon,
9+
} from "@phosphor-icons/react";
10+
import { useRef, useState } from "react";
11+
12+
// Placeholder for the future fulfillment check when the course is added.
13+
const try_add_course = async (_course: CatalogCourse): Promise<string | null> =>
14+
null;
15+
16+
export default function CourseAddPanel() {
17+
const [results, setResults] = useState<CatalogCourse[] | null>(null);
18+
const [selectedId, setSelectedId] = useState<number | null>(null);
19+
const [fulfillmentMessage, setFulfillmentMessage] = useState<string | null>(
20+
null,
21+
);
22+
const [isFulfillmentLoading, setIsFulfillmentLoading] = useState(false);
23+
const fulfillmentRequestId = useRef(0);
24+
25+
// Mark the course as selected, then ask whether it satisfies a requirement.
26+
const selectCourse = async (course: CatalogCourse) => {
27+
const requestId = fulfillmentRequestId.current + 1;
28+
fulfillmentRequestId.current = requestId;
29+
setSelectedId(course.uniqueId);
30+
setFulfillmentMessage(null);
31+
setIsFulfillmentLoading(true);
32+
33+
const message = await try_add_course(course);
34+
if (fulfillmentRequestId.current !== requestId) return;
35+
36+
setFulfillmentMessage(message);
37+
setIsFulfillmentLoading(false);
38+
};
39+
40+
if (results === null) {
41+
return (
42+
<CourseSearchContent
43+
// Search submits populate the list and reset any prior selection state.
44+
onSearchSubmit={async (formData) => {
45+
const found = await searchCatalogCourses(formData);
46+
setResults(found);
47+
setSelectedId(null);
48+
setFulfillmentMessage(null);
49+
setIsFulfillmentLoading(false);
50+
}}
51+
/>
52+
);
53+
}
54+
55+
// Keep the selected course in sync with the current search results.
56+
const selectedCourse = results.find(
57+
(course) => course.uniqueId === selectedId,
58+
);
59+
// Show the fulfillment message only while loading or after the check finishes.
60+
const showFulfillment = isFulfillmentLoading || fulfillmentMessage;
61+
62+
return (
63+
<div className="flex flex-col">
64+
<p className="font-bold text-2xl mb-4">Add courses</p>
65+
<button
66+
type="button"
67+
onClick={() => {
68+
fulfillmentRequestId.current += 1;
69+
setResults(null);
70+
setSelectedId(null);
71+
setFulfillmentMessage(null);
72+
setIsFulfillmentLoading(false);
73+
}}
74+
className="flex items-center gap-1 text-dap-orange font-semibold text-[13px] uppercase tracking-wide mb-4 hover:underline"
75+
>
76+
<CaretLeftIcon size={16} weight="bold" />
77+
Search Results
78+
</button>
79+
80+
<div className="space-y-3 max-h-[420px] overflow-y-auto pr-1">
81+
{results.map((course) => {
82+
const code = `${course.department} ${course.number}`;
83+
const isSelected = course.uniqueId === selectedId;
84+
85+
return (
86+
<button
87+
key={course.uniqueId}
88+
type="button"
89+
onClick={() => void selectCourse(course)}
90+
className={`w-full min-h-[70px] grid grid-cols-[22px_1fr_44px] items-stretch overflow-hidden rounded-md border bg-white text-left transition-colors ${
91+
isSelected
92+
? "border-2 border-[#579D42]"
93+
: "border-[#E5E1DA] hover:border-gray-300"
94+
}`}
95+
>
96+
<div className="bg-dap-orange flex items-center justify-center">
97+
<DotsSixVerticalIcon
98+
size={16}
99+
weight="bold"
100+
className="text-white"
101+
/>
102+
</div>
103+
<div className="px-3 py-3">
104+
<p className="text-base font-semibold text-gray-900 leading-tight">
105+
{code}
106+
</p>
107+
<p className="mt-1 text-sm font-medium text-black leading-tight uppercase">
108+
{course.fullName}
109+
</p>
110+
</div>
111+
<div className="flex items-center justify-center">
112+
<PlusCircleIcon size={24} className="text-gray-700" />
113+
</div>
114+
</button>
115+
);
116+
})}
117+
</div>
118+
119+
{/* This block appears only after selection and can be used for requirement feedback. */}
120+
{showFulfillment ? (
121+
<div className="mt-7 rounded-md border border-[#63B031] bg-[#F4FFE6] px-5 py-5 text-sm text-black">
122+
{isFulfillmentLoading
123+
? "Checking requirement fit..."
124+
: fulfillmentMessage}
125+
</div>
126+
) : null}
127+
128+
<div className={`${showFulfillment ? "mt-8" : "mt-7"} flex justify-end`}>
129+
<button
130+
type="button"
131+
onClick={() => selectedCourse && void selectCourse(selectedCourse)}
132+
disabled={selectedId === null}
133+
className="flex h-14 items-center justify-center gap-2 rounded-md bg-[#579D42] px-7 text-xl font-bold text-white transition-colors hover:bg-[#4C8F3B] disabled:cursor-not-allowed disabled:opacity-50"
134+
>
135+
<PlusIcon size={28} weight="light" />
136+
Add Course
137+
</button>
138+
</div>
139+
</div>
140+
);
141+
}

entrypoints/degree-audit/components/degree-audit-page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { HStack, VStack } from "@/entrypoints/components/common/helperdivs";
22
import Title from "@/entrypoints/components/common/text";
3-
import { CourseSearchPanel } from "@/entrypoints/components/course-add-modal";
43
import "@/entrypoints/styles/content.css";
54
import { formatMajorLabel } from "@/lib/utils";
65
import { useAuditContext } from "../providers/audit-provider";
6+
import CourseAddPanel from "./course-add-panel";
77
import { SimpleDegreeCompletionDonut } from "./degree-completion-donut";
88
import { CreditHourTotalsCard, GPATotalsCard } from "./gpa-credit-cards";
99
import RequirementBreakdown, {
@@ -44,7 +44,7 @@ const SidePanel = () => {
4444
>
4545
<SimpleDegreeCompletionDonut size={300} />
4646
<div className="w-sm mt-10 p-3 rounded-lg border border-gray-200 bg-[#FAFAF9]">
47-
<CourseSearchPanel />
47+
<CourseAddPanel />
4848
</div>
4949
<VStack gap={4} className="w-sm mt-4">
5050
{gpaRule ? (

0 commit comments

Comments
 (0)