Skip to content
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
10 changes: 9 additions & 1 deletion apps/cli/src/tools/populate-catalog.ts
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import was bugging when i tried to build locally, because different root folder, just copied and pasted the function over

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ import { defineCommand } from "citty";
import { brandIntro, isVerbose, p, pc, setVerbosity } from "../ui";
import { getDb } from "@sneu/db/pg";
import { catalogMajorsT, catalogMinorsT } from "@sneu/db/schema";
import { chunk } from "../../../../packages/scraper/src/upload/types";

export function chunk<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}

/**
* Recursively find all files matching a given filename within a directory.
*/
Expand Down
81 changes: 78 additions & 3 deletions apps/searchneu/app/graduate/page.tsx
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving the guest page to base graduate url

Original file line number Diff line number Diff line change
@@ -1,12 +1,87 @@
import { GuestHeaderClient } from "@/components/graduate/GuestHeaderClient";
import { GuestPlanClient } from "@/components/graduate/GuestPlanClient";
import NewPlanModal from "@/components/graduate/modal/NewPlanModal";
import { auth } from "@/lib/auth/auth";
import { getAuditPlans } from "@/lib/dal/audits";
import { getMajor, getMinor } from "@/lib/dal/catalog";
import { getCourseNamesBatch } from "@/lib/dal/courses";
import { Requirement } from "@/lib/graduate/types";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function Page() {
function collectCourseKeys(reqs: Requirement[], out: Set<string>): void {
for (const req of reqs) {
if (req.type === "COURSE") {
out.add(`${req.subject}-${req.classId}`);
} else if (req.type === "AND" || req.type === "OR" || req.type === "XOM") {
collectCourseKeys(req.courses, out);
} else if (req.type === "SECTION") {
collectCourseKeys(req.requirements, out);
}
}
}

export default async function Page({
searchParams,
}: {
searchParams: Promise<{
majors?: string | string[];
minors?: string | string[];
catalogYear?: string;
courses?: string;
}>;
}) {
const session = await auth.api.getSession({ headers: await headers() });

if (session) {
const plans = await getAuditPlans(session.user.id);
if (plans.length > 0) {
const lastModified = plans.reduce((latest, plan) =>
plan.updatedAt > latest.updatedAt ? plan : latest,
);
redirect(`/graduate/${lastModified.id}`);
}
return (
<div>
<NewPlanModal isGuest={false} />
</div>
);
}

const params = await searchParams;
const toArray = (v: string | string[] | undefined): string[] => {
if (!v) return [];
return Array.isArray(v) ? v.filter(Boolean) : [v];
};
const majorNames = toArray(params.majors);
const minorNames = toArray(params.minors);
const catalogYear = params.catalogYear ? Number(params.catalogYear) : null;
const scheduleCourseKeys = params.courses
? params.courses.split(",").filter(Boolean)
: [];

let courseNames: Record<string, string> = {};
if (catalogYear) {
const [majors, minors] = await Promise.all([
Promise.all(majorNames.map((m) => getMajor(catalogYear, m))),
Promise.all(minorNames.map((m) => getMinor(catalogYear, m))),
]);

const keys = new Set<string>(scheduleCourseKeys);
for (const m of [...majors, ...minors]) {
if (!m) continue;
for (const section of m.requirementSections) {
collectCourseKeys(section.requirements, keys);
}
}

courseNames = await getCourseNamesBatch(keys);
}

return (
<div>
<NewPlanModal isGuest={session == null} />
<div className="flex min-h-0 w-full flex-1 flex-col gap-4 px-6">
<GuestHeaderClient />
<GuestPlanClient initialCourseNames={courseNames} />
</div>
);
}
76 changes: 9 additions & 67 deletions apps/searchneu/components/graduate/BasePlanClient.tsx
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code cleanup, moving fucntions to plan util file

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
Major,
Minor,
SeasonEnum,
StatusEnum,
Whiteboard,
} from "@/lib/graduate/types";
import {
Expand All @@ -28,7 +27,11 @@ import {
collisionAlgorithm,
nextId,
DELETE_ZONE_ID,
addYear,
deleteYear,
removeCourse,
} from "@/lib/graduate/planUtils";
import { pruneWhiteboard } from "@/lib/graduate/requirementUtils";
import { CourseNameContext } from "./CourseNameContext";
import { CourseDetailsContext } from "./CourseDetailsContext";
import type { CourseDetails } from "@/lib/graduate/types";
Expand Down Expand Up @@ -79,30 +82,6 @@ export function BasePlanClient({
"whiteboard",
);

/** Collect all "SUBJECT CLASSID" keys present in a schedule. */
function scheduleCourseKeys(audit: Audit): Set<string> {
const keys = new Set<string>();
for (const y of audit.years) {
for (const t of [y.fall, y.spring, y.summer1, y.summer2]) {
for (const c of t.classes) keys.add(`${c.subject} ${c.classId}`);
}
}
return keys;
}

/** Remove whiteboard courses that no longer exist in the schedule. */
function pruneWhiteboard(audit: Audit, wb: Whiteboard): Whiteboard | null {
const valid = scheduleCourseKeys(audit);
let changed = false;
const pruned: Whiteboard = {};
for (const [section, entry] of Object.entries(wb)) {
const filtered = entry.courses.filter((k) => valid.has(k));
if (filtered.length !== entry.courses.length) changed = true;
pruned[section] = { ...entry, courses: filtered };
}
return changed ? pruned : null;
}

function persist(updated: Audit) {
const withIds = assignDndIds(updated);
setSchedule(withIds);
Expand Down Expand Up @@ -239,32 +218,11 @@ export function BasePlanClient({
season: SeasonEnum,
courseIndex: number,
) {
const updated = produce(schedule, (draft) => {
const year = draft.years.find((y) => y.year === yearNum);
if (!year) return;
const termMap: Record<string, AuditTerm> = {
[SeasonEnum.FL]: year.fall,
[SeasonEnum.SP]: year.spring,
[SeasonEnum.S1]: year.summer1,
[SeasonEnum.S2]: year.summer2,
};
const term = termMap[season];
if (term) {
term.classes.splice(courseIndex, 1);
}
});
persist(updated);
persist(removeCourse(schedule, yearNum, season, courseIndex));
}

function handleDeleteYear(yearNum: number) {
const updated = produce(schedule, (draft) => {
const idx = yearNum - 1;
if (idx >= draft.years.length) return;
draft.years.splice(idx, 1);
draft.years.forEach((y, i) => {
y.year = i + 1;
});
});
const updated = deleteYear(schedule, yearNum);
setExpandedYears((prev) => {
const next = new Set<number>();
for (let i = 1; i <= updated.years.length; i++) {
Expand All @@ -277,24 +235,8 @@ export function BasePlanClient({
}

function handleAddYear() {
const nextYear = schedule.years.length + 1;
const emptyTerm = (season: SeasonEnum): AuditTerm => ({
season,
status: StatusEnum.CLASSES,
classes: [],
id: `${nextYear}-${season}`,
});
const updated = produce(schedule, (draft) => {
draft.years.push({
year: nextYear,
fall: emptyTerm(SeasonEnum.FL),
spring: emptyTerm(SeasonEnum.SP),
summer1: emptyTerm(SeasonEnum.S1),
summer2: emptyTerm(SeasonEnum.S2),
isSummerFull: false,
});
});
setExpandedYears((prev) => new Set([...prev, nextYear]));
const { schedule: updated, addedYear } = addYear(schedule);
setExpandedYears((prev) => new Set([...prev, addedYear]));
persist(updated);
}

Expand All @@ -308,7 +250,7 @@ export function BasePlanClient({
>
<DeleteDropZone>
<div className="flex min-h-0 flex-1 overflow-hidden">
<div className="bg-neu25 w-[360px] flex-shrink-0 overflow-y-auto">
<div className="bg-neu25 h-[80vh] w-[360px] flex-shrink-0 pb-10">
<div className="flex justify-center gap-1 px-4 pt-2">
<button
type="button"
Expand Down
2 changes: 2 additions & 0 deletions apps/searchneu/components/graduate/GuestHeaderClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export function GuestHeaderClient() {
const [guestPlan, setGuestPlan] =
useLocalStorage<CreateAuditPlanInput | null>("guest-plan", null);

if (!guestPlan) return null;

async function handleDelete() {
if (!confirm(`Delete "${guestPlan?.name}"?`)) {
return;
Expand Down
15 changes: 4 additions & 11 deletions apps/searchneu/components/graduate/GuestPlanClient.tsx
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

support for guest plan client on base /graduate url

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use client";

import { useCallback, useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { Audit, Major, Minor, Whiteboard } from "@/lib/graduate/types";
import { useCallback, useMemo } from "react";
import { Audit, Whiteboard, Major, Minor } from "@/lib/graduate/types";
import { useLocalStorage } from "@/lib/graduate/useLocalStorage";
import { CreateAuditPlanInput } from "@/lib/graduate/api-dtos";
import { BasePlanClient } from "./BasePlanClient";
import NewPlanModal from "./modal/NewPlanModal";

const COURSE_NAMES_KEY = "guest-plan-courseNames";

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

useEffect(() => {
if (!guestPlan) {
router.replace("/graduate");
}
}, [guestPlan, router]);

// Persist server-provided course names to localStorage; on subsequent
// visits (no search params) fall back to the cached copy.
const courseNames = useMemo(() => {
Expand Down Expand Up @@ -74,7 +67,7 @@ export function GuestPlanClient({
[setGuestPlan],
);

if (!guestPlan) return null;
if (!guestPlan) return <NewPlanModal isGuest={true} />;

return (
<BasePlanClient
Expand Down
2 changes: 1 addition & 1 deletion apps/searchneu/components/graduate/modal/EditPlanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ export default function EditPlanModal({

toast(`Plan "${updatedPlan.name}" updated successfully!`);
onOpenChange(false);
router.push(`/graduate/guest?${queryParams.toString()}`);
router.push(`/graduate?${queryParams.toString()}`);
return;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/searchneu/components/graduate/modal/NewPlanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export default function NewPlanModal({
queryParams.set("courses", [...courseKeys].join(","));

toast(`Plan ${newPlan.name} created locally! Redirecting...`);
router.push(`/graduate/guest?${queryParams.toString()}`);
router.push(`/graduate?${queryParams.toString()}`);
return;
}

Expand Down
Loading
Loading