Skip to content

Commit 57c2118

Browse files
committed
graduate cleanup + qol
graduate authed redirect graduate guest mode auto load plan remove validation, sidebar cleanup, dto cleanup tests for dtos and requirement utils using dal catalog
1 parent e47df5b commit 57c2118

25 files changed

Lines changed: 1067 additions & 4214 deletions
Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,87 @@
1+
import { GuestHeaderClient } from "@/components/graduate/GuestHeaderClient";
2+
import { GuestPlanClient } from "@/components/graduate/GuestPlanClient";
13
import NewPlanModal from "@/components/graduate/modal/NewPlanModal";
24
import { auth } from "@/lib/auth/auth";
5+
import { getAuditPlans } from "@/lib/dal/audits";
6+
import { getMajor, getMinor } from "@/lib/dal/catalog";
7+
import { getCourseNamesBatch } from "@/lib/dal/courses";
8+
import { Requirement } from "@/lib/graduate/types";
39
import { headers } from "next/headers";
10+
import { redirect } from "next/navigation";
411

5-
export default async function Page() {
12+
function collectCourseKeys(reqs: Requirement[], out: Set<string>): void {
13+
for (const req of reqs) {
14+
if (req.type === "COURSE") {
15+
out.add(`${req.subject}-${req.classId}`);
16+
} else if (req.type === "AND" || req.type === "OR" || req.type === "XOM") {
17+
collectCourseKeys(req.courses, out);
18+
} else if (req.type === "SECTION") {
19+
collectCourseKeys(req.requirements, out);
20+
}
21+
}
22+
}
23+
24+
export default async function Page({
25+
searchParams,
26+
}: {
27+
searchParams: Promise<{
28+
majors?: string | string[];
29+
minors?: string | string[];
30+
catalogYear?: string;
31+
courses?: string;
32+
}>;
33+
}) {
634
const session = await auth.api.getSession({ headers: await headers() });
35+
36+
if (session) {
37+
const plans = await getAuditPlans(session.user.id);
38+
if (plans.length > 0) {
39+
const lastModified = plans.reduce((latest, plan) =>
40+
plan.updatedAt > latest.updatedAt ? plan : latest,
41+
);
42+
redirect(`/graduate/${lastModified.id}`);
43+
}
44+
return (
45+
<div>
46+
<NewPlanModal isGuest={false} />
47+
</div>
48+
);
49+
}
50+
51+
const params = await searchParams;
52+
const toArray = (v: string | string[] | undefined): string[] => {
53+
if (!v) return [];
54+
return Array.isArray(v) ? v.filter(Boolean) : [v];
55+
};
56+
const majorNames = toArray(params.majors);
57+
const minorNames = toArray(params.minors);
58+
const catalogYear = params.catalogYear ? Number(params.catalogYear) : null;
59+
const scheduleCourseKeys = params.courses
60+
? params.courses.split(",").filter(Boolean)
61+
: [];
62+
63+
let courseNames: Record<string, string> = {};
64+
if (catalogYear) {
65+
const [majors, minors] = await Promise.all([
66+
Promise.all(majorNames.map((m) => getMajor(catalogYear, m))),
67+
Promise.all(minorNames.map((m) => getMinor(catalogYear, m))),
68+
]);
69+
70+
const keys = new Set<string>(scheduleCourseKeys);
71+
for (const m of [...majors, ...minors]) {
72+
if (!m) continue;
73+
for (const section of m.requirementSections) {
74+
collectCourseKeys(section.requirements, keys);
75+
}
76+
}
77+
78+
courseNames = await getCourseNamesBatch(keys);
79+
}
80+
781
return (
8-
<div>
9-
<NewPlanModal isGuest={session == null} />
82+
<div className="flex min-h-0 w-full flex-1 flex-col gap-4 px-6">
83+
<GuestHeaderClient />
84+
<GuestPlanClient initialCourseNames={courseNames} />
1085
</div>
1186
);
1287
}

apps/searchneu/components/graduate/BasePlanClient.tsx

Lines changed: 9 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
Major,
1818
Minor,
1919
SeasonEnum,
20-
StatusEnum,
2120
Whiteboard,
2221
} from "@/lib/graduate/types";
2322
import {
@@ -28,7 +27,11 @@ import {
2827
collisionAlgorithm,
2928
nextId,
3029
DELETE_ZONE_ID,
30+
addYear,
31+
deleteYear,
32+
removeCourse,
3133
} from "@/lib/graduate/planUtils";
34+
import { pruneWhiteboard } from "@/lib/graduate/requirementUtils";
3235
import { CourseNameContext } from "./CourseNameContext";
3336
import { CourseDetailsContext } from "./CourseDetailsContext";
3437
import type { CourseDetails } from "@/lib/graduate/types";
@@ -79,30 +82,6 @@ export function BasePlanClient({
7982
"whiteboard",
8083
);
8184

82-
/** Collect all "SUBJECT CLASSID" keys present in a schedule. */
83-
function scheduleCourseKeys(audit: Audit): Set<string> {
84-
const keys = new Set<string>();
85-
for (const y of audit.years) {
86-
for (const t of [y.fall, y.spring, y.summer1, y.summer2]) {
87-
for (const c of t.classes) keys.add(`${c.subject} ${c.classId}`);
88-
}
89-
}
90-
return keys;
91-
}
92-
93-
/** Remove whiteboard courses that no longer exist in the schedule. */
94-
function pruneWhiteboard(audit: Audit, wb: Whiteboard): Whiteboard | null {
95-
const valid = scheduleCourseKeys(audit);
96-
let changed = false;
97-
const pruned: Whiteboard = {};
98-
for (const [section, entry] of Object.entries(wb)) {
99-
const filtered = entry.courses.filter((k) => valid.has(k));
100-
if (filtered.length !== entry.courses.length) changed = true;
101-
pruned[section] = { ...entry, courses: filtered };
102-
}
103-
return changed ? pruned : null;
104-
}
105-
10685
function persist(updated: Audit) {
10786
const withIds = assignDndIds(updated);
10887
setSchedule(withIds);
@@ -239,32 +218,11 @@ export function BasePlanClient({
239218
season: SeasonEnum,
240219
courseIndex: number,
241220
) {
242-
const updated = produce(schedule, (draft) => {
243-
const year = draft.years.find((y) => y.year === yearNum);
244-
if (!year) return;
245-
const termMap: Record<string, AuditTerm> = {
246-
[SeasonEnum.FL]: year.fall,
247-
[SeasonEnum.SP]: year.spring,
248-
[SeasonEnum.S1]: year.summer1,
249-
[SeasonEnum.S2]: year.summer2,
250-
};
251-
const term = termMap[season];
252-
if (term) {
253-
term.classes.splice(courseIndex, 1);
254-
}
255-
});
256-
persist(updated);
221+
persist(removeCourse(schedule, yearNum, season, courseIndex));
257222
}
258223

259224
function handleDeleteYear(yearNum: number) {
260-
const updated = produce(schedule, (draft) => {
261-
const idx = yearNum - 1;
262-
if (idx >= draft.years.length) return;
263-
draft.years.splice(idx, 1);
264-
draft.years.forEach((y, i) => {
265-
y.year = i + 1;
266-
});
267-
});
225+
const updated = deleteYear(schedule, yearNum);
268226
setExpandedYears((prev) => {
269227
const next = new Set<number>();
270228
for (let i = 1; i <= updated.years.length; i++) {
@@ -277,24 +235,8 @@ export function BasePlanClient({
277235
}
278236

279237
function handleAddYear() {
280-
const nextYear = schedule.years.length + 1;
281-
const emptyTerm = (season: SeasonEnum): AuditTerm => ({
282-
season,
283-
status: StatusEnum.CLASSES,
284-
classes: [],
285-
id: `${nextYear}-${season}`,
286-
});
287-
const updated = produce(schedule, (draft) => {
288-
draft.years.push({
289-
year: nextYear,
290-
fall: emptyTerm(SeasonEnum.FL),
291-
spring: emptyTerm(SeasonEnum.SP),
292-
summer1: emptyTerm(SeasonEnum.S1),
293-
summer2: emptyTerm(SeasonEnum.S2),
294-
isSummerFull: false,
295-
});
296-
});
297-
setExpandedYears((prev) => new Set([...prev, nextYear]));
238+
const { schedule: updated, addedYear } = addYear(schedule);
239+
setExpandedYears((prev) => new Set([...prev, addedYear]));
298240
persist(updated);
299241
}
300242

@@ -308,7 +250,7 @@ export function BasePlanClient({
308250
>
309251
<DeleteDropZone>
310252
<div className="flex min-h-0 flex-1 overflow-hidden">
311-
<div className="bg-neu25 w-[360px] flex-shrink-0 overflow-y-auto">
253+
<div className="bg-neu25 h-[80vh] w-[360px] flex-shrink-0 pb-10">
312254
<div className="flex justify-center gap-1 px-4 pt-2">
313255
<button
314256
type="button"

apps/searchneu/components/graduate/GuestHeaderClient.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export function GuestHeaderClient() {
2323
const [guestPlan, setGuestPlan] =
2424
useLocalStorage<CreateAuditPlanInput | null>("guest-plan", null);
2525

26+
if (!guestPlan) return null;
27+
2628
async function handleDelete() {
2729
if (!confirm(`Delete "${guestPlan?.name}"?`)) {
2830
return;

apps/searchneu/components/graduate/GuestPlanClient.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"use client";
22

3-
import { useCallback, useEffect, useMemo } from "react";
4-
import { useRouter } from "next/navigation";
5-
import { Audit, Major, Minor, Whiteboard } from "@/lib/graduate/types";
3+
import { useCallback, useMemo } from "react";
4+
import { Audit, Whiteboard, Major, Minor } from "@/lib/graduate/types";
65
import { useLocalStorage } from "@/lib/graduate/useLocalStorage";
76
import { CreateAuditPlanInput } from "@/lib/graduate/api-dtos";
87
import { BasePlanClient } from "./BasePlanClient";
8+
import NewPlanModal from "./modal/NewPlanModal";
99

1010
const COURSE_NAMES_KEY = "guest-plan-courseNames";
1111

@@ -20,17 +20,10 @@ export function GuestPlanClient({
2020
initialMajors = [],
2121
initialMinors = [],
2222
}: GuestPlanClientProps) {
23-
const router = useRouter();
2423
const [guestPlan, setGuestPlan] = useLocalStorage<
2524
(CreateAuditPlanInput & { whiteboard?: Whiteboard }) | null
2625
>("guest-plan", null);
2726

28-
useEffect(() => {
29-
if (!guestPlan) {
30-
router.replace("/graduate");
31-
}
32-
}, [guestPlan, router]);
33-
3427
// Persist server-provided course names to localStorage; on subsequent
3528
// visits (no search params) fall back to the cached copy.
3629
const courseNames = useMemo(() => {
@@ -74,7 +67,7 @@ export function GuestPlanClient({
7467
[setGuestPlan],
7568
);
7669

77-
if (!guestPlan) return null;
70+
if (!guestPlan) return <NewPlanModal isGuest={true} />;
7871

7972
return (
8073
<BasePlanClient

apps/searchneu/components/graduate/modal/EditPlanModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ export default function EditPlanModal({
258258

259259
toast(`Plan "${updatedPlan.name}" updated successfully!`);
260260
onOpenChange(false);
261-
router.push(`/graduate/guest?${queryParams.toString()}`);
261+
router.push(`/graduate?${queryParams.toString()}`);
262262
return;
263263
}
264264

apps/searchneu/components/graduate/modal/NewPlanModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export default function NewPlanModal({
329329
queryParams.set("courses", [...courseKeys].join(","));
330330

331331
toast(`Plan ${newPlan.name} created locally! Redirecting...`);
332-
router.push(`/graduate/guest?${queryParams.toString()}`);
332+
router.push(`/graduate?${queryParams.toString()}`);
333333
return;
334334
}
335335

0 commit comments

Comments
 (0)