diff --git a/app/actions/actions.ts b/app/actions/actions.ts index 039a52d..5802437 100644 --- a/app/actions/actions.ts +++ b/app/actions/actions.ts @@ -1,5 +1,6 @@ "use server"; +import prisma from "@/lib/prisma"; import { cookies } from "next/headers"; export async function setPlanCookie(plan: string) { @@ -14,3 +15,16 @@ export async function getPlanCookie() { return out; } + +export async function setSelectedCookie(selected: any) { + (await cookies()).set("selectedCourses", selected); +} + +export async function getSelectedCoursesCookie() { + const cookieStore = await cookies(); + const plan = cookieStore.get("selectedCourses"); + + const out = plan ? plan.value : undefined; + + return out; +} diff --git a/app/actions/getCourses.ts b/app/actions/getCourses.ts index 0c5dd2f..2ff1e39 100644 --- a/app/actions/getCourses.ts +++ b/app/actions/getCourses.ts @@ -4,7 +4,7 @@ import { cookies } from "next/headers"; import prisma from "../../lib/prisma"; import { Prisma } from "@prisma/client"; import { auth } from "@/lib/auth"; -import { getPlanCookie } from "./actions"; +import { getPlanCookie, setSelectedCookie } from "./actions"; export async function getUniqueCodes() { const codes = await prisma.sectionAttribute.findMany(); @@ -66,34 +66,84 @@ export async function getPlanCourses1() { const planCookie: any = await getPlanCookie(); const session = await auth(); - return await prisma.coursePlan.findMany({ - where: { - AND: { + if (planCookie) { + return await prisma.coursePlan.findUnique({ + where: { User: { uuid: session?.user.id, }, - //id: parseInt(planCookie), + id: parseInt(planCookie), + }, + include: { + courses: true, }, + }); + } + return {}; +} + +export async function removeCourseFromDBPlan(course: any) { + const id: any = await getPlanCookie(); + //let DOTW: Array = dotw.split(","); + const updatedCourse = await prisma.coursePlan.update({ + where: { + id: parseInt(id), }, - include: { - courses: true, + data: { + courses: { + disconnect: { + id: course.id, + }, + }, }, }); + getCourseIds(); } -export async function getPlanCourses(planID: any) { +export async function getCourseIds() { + const id: any = await getPlanCookie(); //let DOTW: Array = dotw.split(","); + const output = []; - return await prisma.course.findMany({ - where: { - CoursePlan: { - some: { - id: parseInt(planID), + if (id) { + const ids = await prisma.coursePlan.findUnique({ + where: { + id: parseInt(id), + }, + select: { + courses: { + select: { + id: true, + }, }, }, + }); + + if (ids) { + for (let theid of ids.courses) { + output.push(theid.id); + } + } + } + + //setSelectedCookie(output); + + return output; +} + +export async function updateDBPlan(course: any) { + const id: any = await getPlanCookie(); + //let DOTW: Array = dotw.split(","); + const updatedCourse = await prisma.coursePlan.update({ + where: { + id: parseInt(id), }, - include: { - CoursePlan: true, + data: { + courses: { + connect: { + id: course.id, + }, + }, }, }); } @@ -138,6 +188,32 @@ export async function getInitialCourses( }); } +export async function getCoursePlans() { + const session = await auth(); + const user = await prisma.user.findUnique({ + where: { + uuid: session?.user?.id, + }, + }); + let courses: any; + + if (user) { + courses = await prisma.coursePlan.findMany({ + where: { + User: { + id: user.id, + }, + }, + include: { + courses: true, + }, + }); + } else { + courses = null; + } + return courses; +} + export async function getCourses( take: any, query: any, diff --git a/app/actions/setPlanName.ts b/app/actions/setPlanName.ts new file mode 100644 index 0000000..27382a4 --- /dev/null +++ b/app/actions/setPlanName.ts @@ -0,0 +1,14 @@ +"use server"; + +import prisma from "@/lib/prisma"; + +export async function setPlanName(planName: string, id: string) { + const newPlan = await prisma.coursePlan.update({ + where: { + id: parseInt(id), + }, + data: { + name: planName, + }, + }); +} diff --git a/app/page.tsx b/app/page.tsx index 9f0fc5f..c70cf09 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -12,6 +12,7 @@ import { getTerms, getUniqueStartEndTimes, getUniqueCodes, + getCoursePlans, } from "../app/actions/getCourses"; import { redirect } from "next/navigation"; import { CoursePlan } from "@prisma/client"; @@ -31,7 +32,6 @@ export default async function Page(props: { if (pagePref && pagePref.value != "/") { redirect(pagePref.value); - } const searchParams = await props.searchParams; @@ -41,7 +41,8 @@ export default async function Page(props: { const stime = searchParams?.stime || []; const homePageProps: any = {}; const initalCourses = await getInitialCourses(query, term, dotw, stime); - const planCourses: CoursePlan[] = await getPlanCourses1(); + const planCourses: any = await getPlanCourses1(); + const coursePlans: any = await getCoursePlans(); homePageProps["fullCourseList"] = ( } > - + ); diff --git a/app/sw.ts b/app/sw.ts index 6396c20..fa0a2d3 100644 --- a/app/sw.ts +++ b/app/sw.ts @@ -1,6 +1,9 @@ import { defaultCache } from "@serwist/next/worker"; import type { PrecacheEntry, SerwistGlobalConfig } from "serwist"; import { Serwist } from "serwist"; +import { disableDevLogs } from "serwist"; + +disableDevLogs(); declare global { interface WorkerGlobalScope extends SerwistGlobalConfig { diff --git a/components/CourseCard.tsx b/components/CourseCard.tsx index 854826b..2be4232 100644 --- a/components/CourseCard.tsx +++ b/components/CourseCard.tsx @@ -1,32 +1,37 @@ "use client"; -import { Card, CardBody, CardHeader, Chip, Divider } from "@nextui-org/react"; +import { + Button, + Card, + CardBody, + CardHeader, + Chip, + Divider, +} from "@nextui-org/react"; import Image from "next/image"; import { tv } from "tailwind-variants"; import axios from "axios"; - +import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; import { generateColorFromName } from "../components/primitives"; import { Error } from "@mui/icons-material"; +import { getPlanCookie, setSelectedCookie } from "app/actions/actions"; +import { useEffect, useState } from "react"; +import { getCourseIds, updateDBPlan } from "app/actions/getCourses"; +import { useRouter } from "next/navigation"; + export const card = tv({ slots: { - base: "bg-light_foreground min-h-32 max-h-62 w-[98%] rounded-md scroll-none drop-shadow-lg transition-colors", - role: "font-bold text-primary ", + base: "hover:cursor-pointer bg-light_foreground min-h-32 max-h-62 w-[98%] rounded-md scroll-none drop-shadow-lg transition-colors", + role: "font-bold text-primary ", }, }); const { base, role } = card(); async function updatePlan(course: any) { - console.log("updating"); - await axios - .post("/api/updatePlan", { - course: course, - }) - .then(function (response) { - // Handle response - }) - .catch(function (error) { - console.log(error); - }); + await updateDBPlan(course); + setSelectedCookie(course); //Not actually read anywhere but used to refresh the course list + + //console.log("updating"); } export default function CourseCard(props: any) { @@ -81,129 +86,136 @@ export default function CourseCard(props: any) { ); return ( - -
- updatePlan(props.course)}> -
-
-

- {props.course.courseTitle.replace("&", "&")} -

-

- {props.course.subject} {props.course.courseNumber} |{" "} - {props.course.creditHours} credit(s) - {props.course.sectionAttributes.length > 0 && ( - <> -
-  | {attributeCodes} -
- - )} -

-
-
-
- {props.course.instructor.displayName.replace("'", +
updatePlan(props.course)}> + +
+ +
+
+

+ {props.course.courseTitle.replace("&", "&")} +

+

+ {props.course.subject} {props.course.courseNumber} |{" "} + {props.course.creditHours} credit(s) + {props.course.sectionAttributes.length > 0 && ( + <> +
+  | {attributeCodes} +
+ + )} +

+
+
+
+ {props.course.instructor.displayName.replace( +
-
- + - updatePlan(props.course)}> -
-
- {props.course.facultyMeet.meetingTimes.room ? ( -
-
- {props.course.facultyMeet.meetingTimes.buildingDescription}{" "} - {props.course.facultyMeet.meetingTimes.room} -
-
- {props.course.facultyMeet.meetingTimes ? ( -
-
- {} - {props.course.facultyMeet.meetingTimes.beginTime.slice( - 0, - 2 - ) + - ":" + - props.course.facultyMeet.meetingTimes.beginTime.slice( + +
+
+ {props.course.facultyMeet.meetingTimes.room ? ( +
+
+ {props.course.facultyMeet.meetingTimes.buildingDescription}{" "} + {props.course.facultyMeet.meetingTimes.room} +
+
+ {props.course.facultyMeet.meetingTimes ? ( +
+
+ {} + {props.course.facultyMeet.meetingTimes.beginTime.slice( + 0, 2 - )}{" "} - -{" "} - {props.course.facultyMeet.meetingTimes.endTime.slice( - 0, - 2 - ) + - ":" + - props.course.facultyMeet.meetingTimes.endTime.slice( + ) + + ":" + + props.course.facultyMeet.meetingTimes.beginTime.slice( + 2 + )}{" "} + -{" "} + {props.course.facultyMeet.meetingTimes.endTime.slice( + 0, 2 - )} + ) + + ":" + + props.course.facultyMeet.meetingTimes.endTime.slice( + 2 + )} +
-
- ) : null} + ) : null} +
+ ) : ( +
+

+ Contact your Professor for additional details. +

+
+ )} +
+
{coloredDays}
- ) : ( -
-

- Contact your Professor for additional details. -

-
- )} -
-
{coloredDays}
-
-
-
-
-
Instructor
-
- {props.course.instructor.displayName.replace("'", "'")} +
+
+
+
+ Instructor +
+
+ {props.course.instructor.displayName.replace("'", "'")} +
-
- {props.course.instructor.avgRating == null ? null : ( -
-
- {props.course.instructor.avgRating} + {props.course.instructor.avgRating == null ? null : ( +
+
+ {props.course.instructor.avgRating} +
+
+ )} +
+ {props.course.seatsAvailable == 0 ? ( +
+
+ No available seats left for this section +
+ {/* Use shorter msg for mobile */} +
+ No seats +
+ +
+ ) : ( +
+
+ Seats Available: {props.course.seatsAvailable}
)}
- {props.course.seatsAvailable == 0 ? ( -
-
- No available seats left for this section -
- {/* Use shorter msg for mobile */} -
- No seats -
- -
- ) : ( -
-
- Seats Available: {props.course.seatsAvailable} -
-
- )}
-
- - + + +
); } diff --git a/components/CourseCardAdded.tsx b/components/CourseCardAdded.tsx new file mode 100644 index 0000000..a289ad0 --- /dev/null +++ b/components/CourseCardAdded.tsx @@ -0,0 +1,200 @@ +"use client"; +import { Card, CardBody, CardHeader, Chip, Divider } from "@nextui-org/react"; +import Image from "next/image"; +import { tv } from "tailwind-variants"; +import axios from "axios"; +import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; +import { generateColorFromName } from "./primitives"; +import { Error } from "@mui/icons-material"; +export const card = tv({ + slots: { + base: "hover:cursor-pointer bg-light_foreground min-h-32 max-h-62 w-[98%] rounded-md scroll-none drop-shadow-lg transition-colors", + role: "font-bold text-primary ", + }, +}); + +const { base, role } = card(); + +export default function CourseCardAdded(props: any) { + const color = generateColorFromName(props.course.subject); + + const color_mappings: { [key: string]: string } = { + "0": "#4CB944", + "1": "#fb5607", + "2": "#ff006e", + "3": "#8338ec", + "4": "#3a86ff", + }; + + const days = { + M: props.course.facultyMeet.meetingTimes.monday, + T: props.course.facultyMeet.meetingTimes.tuesday, + W: props.course.facultyMeet.meetingTimes.wednesday, + TH: props.course.facultyMeet.meetingTimes.thursday, + F: props.course.facultyMeet.meetingTimes.friday, + }; + + const coloredDays = Object.entries(days).map((item, index) => { + if (item[1]) { + return ( +
+

+ {item[0]} +

+
+ ); + } + + return null; + }); + + const attributeCodes = props.course.sectionAttributes.map( + (item: any, index: number) => { + if (item) { + return ( + + {" " + item.code + " "} + + ); + } + + return null; + } + ); + + return ( + +
+ +
+
+

+ {props.course.courseTitle.replace("&", "&")} +

+

+ {props.course.subject} {props.course.courseNumber} |{" "} + {props.course.creditHours} credit(s) + {props.course.sectionAttributes.length > 0 && ( + <> +
+  | {attributeCodes} +
+ + )} +

+
+
+
+ {props.course.instructor.displayName.replace("'", +
+
+
+
+ + +
+
+ {props.course.facultyMeet.meetingTimes.room ? ( +
+
+ {props.course.facultyMeet.meetingTimes.buildingDescription}{" "} + {props.course.facultyMeet.meetingTimes.room} +
+
+ {props.course.facultyMeet.meetingTimes ? ( +
+
+ {} + {props.course.facultyMeet.meetingTimes.beginTime.slice( + 0, + 2 + ) + + ":" + + props.course.facultyMeet.meetingTimes.beginTime.slice( + 2 + )}{" "} + -{" "} + {props.course.facultyMeet.meetingTimes.endTime.slice( + 0, + 2 + ) + + ":" + + props.course.facultyMeet.meetingTimes.endTime.slice( + 2 + )} +
+
+ ) : null} +
+
+ ) : ( +
+

+ Contact your Professor for additional details. +

+
+ )} +
+
{coloredDays}
+
+
+
+
+
+
Instructor
+
+ {props.course.instructor.displayName.replace("'", "'")} +
+
+ + {props.course.instructor.avgRating == null ? null : ( +
+
+ {props.course.instructor.avgRating} +
+
+ )} +
+ {props.course.seatsAvailable == 0 ? ( +
+
+ No available seats left for this section +
+ {/* Use shorter msg for mobile */} +
+ No seats +
+ +
+ ) : ( +
+
+ Seats Available: {props.course.seatsAvailable} +
+
+ )} +
+
+
+
+ Added to Plan +
+
+
+ + ); +} diff --git a/components/CreatePlan.tsx b/components/CreatePlan.tsx index 4e068f6..0ccfc95 100644 --- a/components/CreatePlan.tsx +++ b/components/CreatePlan.tsx @@ -9,7 +9,8 @@ import { } from "@nextui-org/react"; import AddIcon from "@mui/icons-material/Add"; import DeleteIcon from "@mui/icons-material/Delete"; -import IosShareIcon from "@mui/icons-material/IosShare"; +import EditIcon from "@mui/icons-material/Edit"; +import SaveIcon from "@mui/icons-material/Save"; import HighlightOffIcon from "@mui/icons-material/HighlightOff"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; @@ -24,7 +25,14 @@ import { useCookies } from "next-client-cookies"; import { setPlanCookie } from "../app/actions/actions"; import { generateColorFromName } from "../components/primitives"; - +import { useDebouncedCallback } from "use-debounce"; +import { setPlanName } from "../app/actions/setPlanName"; +import { + getCourseIds, + getPlanCourses1, + removeCourseFromDBPlan, +} from "app/actions/getCourses"; +import { Course, CoursePlan } from "@prisma/client"; export default function CreatePlan(props: any) { const cookies = useCookies(); const router = useRouter(); @@ -33,21 +41,20 @@ export default function CreatePlan(props: any) { const pathname = usePathname(); const { data: session, status } = useSession(); const [coursePlanName, setCoursePlanName]: any = useState(""); + const [editable, setEditable]: any = useState(""); + const [edit, setEdit]: any = useState(false); + const [courses, setCourses] = useState(); + const [coursePlans, setCoursePlans] = useState( + props.coursePlans + ); const [selectedCoursePlan, setSelectedCoursePlan]: any = useState([]); const [isScrolled, setIsScrolled] = useState(false); const fetcher = (url: any) => fetch(url).then((r) => r.json()); - const { data, isLoading, error } = useSWR("/api/getplancourses", fetcher, { - refreshInterval: 800, - }); - const { - data: coursePlans, - isLoading: coursePlansIsLoading, - error: coursePlansError, - } = useSWR("/api/getcourseplans", fetcher, { - refreshInterval: 2000, - }); + const handleNameChange = useDebouncedCallback((newName: any, id: string) => { + setPlanName(newName, id); + }, 50); async function createPlan() { if (coursePlanName) { @@ -67,19 +74,19 @@ export default function CreatePlan(props: any) { }); } } + async function updateLocalPlan() { + console.log("updating local plan"); + const planCourses: any = await getPlanCourses1(); + setCourses(planCourses.courses); + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + //router.refresh(); + } async function removeCourseFromPlan(plan: any, course: any) { - await axios - .post("/api/getplancourses", { - plan: plan, - course: course, - }) - .then(function (response: any) { - //console.log(response); - router.refresh(); - }) - .catch(function (error) { - console.log(error); - }); + await removeCourseFromDBPlan(course); + await updateLocalPlan(); + router.refresh(); } async function deletePlan() { if (cookies.get("plan")) { @@ -99,6 +106,7 @@ export default function CreatePlan(props: any) { }); } } + const handleSelectionChange = (e: any) => { //console.log(e.target.value); setSelectedCoursePlan([e.target.value]); @@ -107,64 +115,53 @@ export default function CreatePlan(props: any) { }; useEffect(() => { - // Update the document title using the browser API - setSelectedCoursePlan([cookies.get("plan")]); - const objDiv: any = document.getElementById("scrollMe"); + console.log("RUNNING THE SCROll"); + updateLocalPlan(); + }, [props.initialPlan, props.coursePlans, cookies.get("selectedCourses")]); - objDiv.scrollTop = objDiv.scrollHeight; - }, [data, cookies]); + useEffect(() => { + setSelectedCoursePlan([cookies.get("plan")]); + setCourses(props.initialPlan.courses); + }, [props.initialPlan, cookies.get("plan")]); const CoursesList = () => { const output: any = []; - if (isLoading) { - return ; - } - if (data && !isLoading) { - const filtered_data = data.filter( - (course: any) => course.id == selectedCoursePlan[0] - ); + if (courses) { + return courses.map((course: any) => ( + - output.push( - removeCourseFromPlan(selectedCoursePlan, course)} - > -
+ // onClick={() => removeCourseFromPlan(selectedCoursePlan, course)} + > +
- -
- {course.subject} {""} {course.courseNumber} -
- {course.courseTitle.replace(/&/g, "&")} -
-
+ +
+ {course.subject} {""} {course.courseNumber} +
+ {course.courseTitle.replace(/&/g, "&")} +
+
-
-
Select a Plan
+
+ {!edit ? "Select a Plan" : "Edit your plan"} +
- + {!edit ? ( + + ) : null} + {edit ? ( + { + setEditable(event.target.value), + handleNameChange(event.target.value, selectedCoursePlan); + }} + /> + ) : null}
@@ -261,6 +294,7 @@ export default function CreatePlan(props: any) {
diff --git a/components/FullCourseList.tsx b/components/FullCourseList.tsx index 62e9172..ee3005b 100644 --- a/components/FullCourseList.tsx +++ b/components/FullCourseList.tsx @@ -5,9 +5,15 @@ import { useCallback, useEffect, useState } from "react"; import CourseCard from "./CourseCard"; import React from "react"; -import { getCourses } from "../app/actions/getCourses"; +import { getCourses, getCourseIds } from "../app/actions/getCourses"; import { useInView } from "react-intersection-observer"; import { Skeleton } from "@nextui-org/react"; +import { useRouter } from "next/navigation"; +import CourseCardAdded from "./CourseCardAdded"; +import { + getSelectedCoursesCookie, + setSelectedCookie, +} from "app/actions/actions"; const NUMBER_OF_USERS_TO_FETCH = 10; @@ -24,17 +30,22 @@ export function FullCourseList({ dotw: Array; stime: Array; }) { + const router = useRouter(); + const [cursor, setCursor] = useState(1); const [take, setTake] = useState(20); const [isDone, setIsDone] = useState(false); + const [selectedCourses, setSelectedCourses]: any = useState([]); + const [courses, setCourses] = useState(init); const { ref, inView } = useInView(); - const loadMoreUsers = useCallback(async () => { + const loadMoreCourses = useCallback(async () => { // NOTE: if this isn't done every time we get a double take and a // race condition desync, breaking isDone. Maybe we'll have better // logic in the future. + setCursor((cursor) => cursor + NUMBER_OF_USERS_TO_FETCH); setTake((take) => take + NUMBER_OF_USERS_TO_FETCH); @@ -49,29 +60,54 @@ export function FullCourseList({ setIsDone(false); } setCourses(apiCourses); + // Prevent an infinite loop. TODO: better solution. // eslint-disable-next-line react-hooks/exhaustive-deps }, [query, term, dotw, stime, inView]); + async function loadCourseIds() { + console.log("LOADING IDS..."); + //const coursesInPlan = await getCourseIds(); + // let daCookie: any = await getSelectedCoursesCookie(); + + //setSelectedCourses(daCookie.split(",")); + const ids = await getCourseIds(); + setSelectedCourses(ids); + //console.log(daCookie.split(",")); + //setSelectedCourses(daCookie.split(",")); + } + useEffect(() => { if (inView) { - loadMoreUsers(); + loadMoreCourses(); + loadCourseIds(); } - }, [inView, loadMoreUsers]); + }, [inView, loadMoreCourses]); useEffect(() => { + console.log("hello world"); setIsDone(false); - loadMoreUsers(); - }, [query, term, dotw, stime, loadMoreUsers]); + loadMoreCourses(); + loadCourseIds(); + }, [query, term, dotw, stime, loadMoreCourses, getCourseIds]); return ( <>
- {courses?.map((course: any) => ( -
- -
- ))} + {courses?.map((course: any) => + selectedCourses?.includes(course.id) ? ( +
+ loadCourseIds()} + /> +
+ ) : ( +
loadCourseIds()}> + +
+ ) + )}
{isDone ? ( <> diff --git a/components/PlanCardList.tsx b/components/PlanCardList.tsx deleted file mode 100644 index a970306..0000000 --- a/components/PlanCardList.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Course } from "@prisma/client"; - -import prisma from "../lib/prisma"; - -import PlanCard from "./PlanCard"; - -async function getPlanCourses(planID: any) { - //let DOTW: Array = dotw.split(","); - - return await prisma.course.findMany({ - where: { - CoursePlan: { - some: { - id: parseInt(planID), - }, - }, - }, - include: { - CoursePlan: true, - }, - }); -} - -export async function PlanCardList({ planID }: { planID: any }) { - const courseList: Course[] = await getPlanCourses(planID); - - return ( - <> -
- {courseList?.map((course: any) => ( -
- -
- ))} -
- - ); -}