diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 53515c3..ba402c2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,21 +4,21 @@ name: Mirror on: push: - branches: [ "main" ] + branches: ["main"] jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: action-pack/gitlab-sync@v3 - with: - # GitLab repo URL - url: https://gitlab.sccs.swarthmore.edu/sccs/scheduler.git - # GitLab username - username: mirrorbot - # GitLab token - token: ${{ secrets.GITLAB_TOKEN }} + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: action-pack/gitlab-sync@v3 + with: + # GitLab repo URL + url: https://gitlab.sccs.swarthmore.edu/sccs/planner.git + # GitLab username + username: mirrorbot + # GitLab token + token: ${{ secrets.GITLAB_TOKEN }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cc6cd89..1c314ca 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,11 +21,11 @@ build: stage: build script: - docker compose -f docker-compose.yml build - - docker push $SCCS_REGISTRY/sccs/scheduler/scheduler:latest + - docker push $SCCS_REGISTRY/sccs/planner/planner:latest deploy_docker_stage: stage: deploy variables: DOCKER_HOST: "tcp://130.58.218.21:2376" script: - - docker stack deploy --with-registry-auth -c ./docker-compose.yml scheduler + - docker stack deploy --with-registry-auth -c ./docker-compose.yml planner diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d22ebbd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing + +Thanks for considering contributing to Scheduler-V2! + +## Opening issues + +If you find a bug, please feel free to [open an issue](https://github.com/swat-sccs/scheduler-v2/issues). + +If you taking the time to mention a problem, even a seemingly minor one, it is greatly appreciated, and a totally valid contribution to this project. Thank you! + +## Fixing bugs + +We love pull requests. Here’s a quick guide: + +1. [Fork this repository](https://github.com/swat-sccs/scheduler-v2/fork) and then clone it locally: + + ```bash + git clone --recursive https://github.com/swat-sccs/scheduler-v2.git + ``` + +2. Create a topic branch for your changes: + + ```bash + git checkout -b fix-for-that-thing + ``` +3. Commit a failing test for the bug: + + ```bash + git commit -am "Adds a failing test to demonstrate that thing" + ``` + +4. Commit a fix that makes the test pass: + + ```bash + git commit -am "Adds a fix for that thing!" + ``` + +6. If everything looks good, push to your fork: + + ```bash + git push origin fix-for-that-thing + ``` + +7. [Submit a pull request.](https://github.com/swat-sccs/scheduler-v2/pulls) + + +## Adding new features + +Thinking of adding a new feature? Awesome! [open an issue](https://github.com/swat-sccs/scheduler-v2/issues). and let’s work on it together! diff --git a/README.md b/README.md index 72972ff..c7c538d 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,57 @@ -# Scheduler v2 +
-## Technologies Used + -- [Next.js 14](https://nextjs.org/docs/getting-started) -- [NextUI v2](https://nextui.org/) -- [Tailwind CSS](https://tailwindcss.com/) -- [Tailwind Variants](https://tailwind-variants.org) -- [TypeScript](https://www.typescriptlang.org/) -- [Framer Motion](https://www.framer.com/motion/) -- [next-themes](https://github.com/pacocoursey/next-themes) -- [Golang](https://go.dev/) +

SCCS Course Planner

+ +

The SCCS Course Planner is an all in one solution for planning your classes at Swarthmore College!

+ +![repo_last_commit] +[![License][repo_license_img]][repo_license_url] +![repo_size] +![build_status] + +

Looking to plan your classes? Visit the live site!

+
-### Install dependencies +## 🏁 Getting Started -Install [Golang](https://go.dev/dl/) +### Install -Install [NodeJS](https://nodejs.org/en) v18.18 or higher + + +### Clone the Repo -### Clone the Repo(recursivly!) ```bash - git clone --recursive https://github.com/swat-sccs/scheduler-v2.git - cd scheduler-v2 + git clone --recursive https://github.com/swat-sccs/planner.git + git checkout dev + cd planner ``` ### Configure your .env file + Paste the following into a .env in the root of the project. -```env -DATABASE_URL="postgresql://postgres:example@localhost:5432/scheduler_db" +```bash +echo 'DATABASE_URL="postgresql://postgres:example@localhost:5432/planner_db"' > .env +``` + +Pase the following into a .env in the /swatscraper dir + +```bash + echo 'HOST=localhost + SQL_USER=postgres + PASS=example + DBNAME=planner_db + OPMODE="DEV"' > ./swatscraper/.env ``` ### Run the development server @@ -42,30 +66,40 @@ first run only: ```bash go mod init github.com/swatscraper go mod tidy -``` - -```bash go run main.go -semester=spring -year=2025 # Change to semester of choice - ``` - - -### View the dev site +## View the dev site Head on over to http://localhost:3000 - - ### (Optional) View the database visually and in the browser! ```bash npx prisma studio ``` -Head on over to http://localhost:5555. Use this to confirm your database is populated. +Head on over to http://localhost:5555. Use this to confirm your database is populated. + +## 📡 Technologies in Use +- [Next.js 14](https://nextjs.org/docs/getting-started) +- [NextUI v2](https://nextui.org/) +- [Tailwind CSS](https://tailwindcss.com/) +- [Tailwind Variants](https://tailwind-variants.org) +- [TypeScript](https://www.typescriptlang.org/) +- [Framer Motion](https://www.framer.com/motion/) +- [next-themes](https://github.com/pacocoursey/next-themes) +- [Golang](https://go.dev/) ## License -Licensed under the [MIT license](https://github.com/swat-sccs/scheduler-v2/blob/main/LICENSE). +Licensed under the [MIT license](https://github.com/swat-sccs/planner/blob/main/LICENSE). + + + +[repo_license_img]: https://img.shields.io/badge/license-Mit-red?style=for-the-badge&logo=none +[repo_license_url]: https://github.com/swat-sccs/planner?tab=MIT-1-ov-file#readme +[repo_last_commit]: https://img.shields.io/github/last-commit/swat-sccs/planner?style=for-the-badge&link=https%3A%2F%2Fgithub.com%2Fswat-sccs%2Fplanner&color=%2343AA8B +[build_status]: https://img.shields.io/github/check-runs/swat-sccs/planner/main?style=for-the-badge&label=Build&color=%2343AA8B +[repo_size]: https://img.shields.io/github/repo-size/swat-sccs/planner?style=for-the-badge diff --git a/app/actions/getCourses.ts b/app/actions/getCourses.ts index 8470c31..9be0a29 100644 --- a/app/actions/getCourses.ts +++ b/app/actions/getCourses.ts @@ -3,14 +3,130 @@ import { cookies } from "next/headers"; import prisma from "../../lib/prisma"; import { Prisma } from "@prisma/client"; +import { auth } from "@/lib/auth"; +import { getPlanCookie } from "./actions"; + +export async function getUniqueCodes() { + const codes = await prisma.sectionAttribute.findMany(); + const daCodes: any = []; + + for (let i = 0; i < codes.length; i++) { + if (!daCodes.includes(codes[i].code)) { + daCodes.push(codes[i].code); + } + } + + return daCodes; +} + +export async function getUniqueStartEndTimes() { + const meetingTimes = await prisma.meetingTime.findMany({ + where: { + beginTime: { not: "" }, + }, + orderBy: { + beginTime: "asc", + }, + }); + const startTimes: any = []; + const endTimes: any = []; + + for (let i = 0; i < meetingTimes.length; i++) { + if (!startTimes.includes(meetingTimes[i].beginTime)) { + startTimes.push(meetingTimes[i].beginTime); + } + if (!endTimes.includes(meetingTimes[i].endTime)) { + endTimes.push(meetingTimes[i].endTime); + } + } + + const times = { startTimes: startTimes, endTimes: endTimes }; + + return times; +} + +export async function getTerms() { + const courses = await prisma.course.findMany(); + const output: any = []; + + for (let i = 0; i < courses.length; i++) { + if (!output.includes(courses[i].year)) { + output.push(courses[i].year); + } + } + + return output; +} + export async function setPlanCookie(plan: string) { (await cookies()).set("plan", plan); } -export async function getInitialCourses() { +export async function getPlanCourses1() { + const planCookie: any = await getPlanCookie(); + const session = await auth(); + + return await prisma.coursePlan.findMany({ + where: { + AND: { + User: { + uuid: session?.user.id, + }, + //id: parseInt(planCookie), + }, + }, + include: { + courses: true, + }, + }); +} + +export async function getPlanCourses(planID: any) { + //let DOTW: Array = dotw.split(","); + return await prisma.course.findMany({ relationLoadStrategy: "join", // or 'query' - take: 10, + where: { + CoursePlan: { + some: { + id: parseInt(planID), + }, + }, + }, + include: { + CoursePlan: true, + }, + }); +} + +export async function getInitialCoursePlans() { + //let DOTW: Array = dotw.split(","); + + return await prisma.coursePlan.findMany({ + relationLoadStrategy: "join", // or 'query' + include: { + courses: true, + }, + }); +} + +export async function getInitialCourses( + query: any, + term: any, + dotw: any, + stime: any +) { + const startTime = stime.toString().split(",").filter(Number); + return await prisma.course.findMany({ + relationLoadStrategy: "join", // or 'query' + take: 20, + where: { + ...(term + ? { + year: term, + } + : {}), + }, include: { sectionAttributes: true, @@ -26,13 +142,13 @@ export async function getInitialCourses() { export async function getCourses( take: any, - cursor: any, query: any, term: any, dotw: any, stime: any ) { const startTime = stime.toString().split(",").filter(Number); + return await prisma.course.findMany({ relationLoadStrategy: "join", // or 'query' take: take, @@ -47,15 +163,19 @@ export async function getCourses( instructor: true, }, - orderBy: [ - { - _relevance: { - fields: ["courseTitle", "subject", "courseNumber"], - search: query.trim().split(" ").join(" & "), - sort: "desc", - }, - }, - ], + ...(query + ? { + orderBy: [ + { + _relevance: { + fields: ["courseTitle", "subject", "courseNumber"], + search: query.trim().split(" ").join(" & "), + sort: "desc", + }, + }, + ], + } + : ""), where: { ...(term ? { diff --git a/app/page.tsx b/app/page.tsx index e13e2d6..9f0fc5f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,61 +6,15 @@ import { cookies } from "next/headers"; import Search from "../components/Search"; import { FullCourseList } from "../components/FullCourseList"; import CreatePlan from "../components/CreatePlan"; -import prisma from "../lib/prisma"; -import { getInitialCourses } from "../app/actions/getCourses"; +import { + getInitialCourses, + getPlanCourses1, + getTerms, + getUniqueStartEndTimes, + getUniqueCodes, +} from "../app/actions/getCourses"; import { redirect } from "next/navigation"; - -async function getCourses() { - const courses = await prisma.course.findMany(); - const output: any = []; - - for (let i = 0; i < courses.length; i++) { - if (!output.includes(courses[i].year)) { - output.push(courses[i].year); - } - } - - return output; -} - -async function getUniqueStartEndTimes() { - const meetingTimes = await prisma.meetingTime.findMany({ - where: { - beginTime: { not: "" }, - }, - orderBy: { - beginTime: "asc", - }, - }); - const startTimes: any = []; - const endTimes: any = []; - - for (let i = 0; i < meetingTimes.length; i++) { - if (!startTimes.includes(meetingTimes[i].beginTime)) { - startTimes.push(meetingTimes[i].beginTime); - } - if (!endTimes.includes(meetingTimes[i].endTime)) { - endTimes.push(meetingTimes[i].endTime); - } - } - - const times = { startTimes: startTimes, endTimes: endTimes }; - - return times; -} - -async function getUniquCodes() { - const codes = await prisma.sectionAttribute.findMany(); - const daCodes: any = []; - - for (let i = 0; i < codes.length; i++) { - if (!daCodes.includes(codes[i].code)) { - daCodes.push(codes[i].code); - } - } - - return daCodes; -} +import { CoursePlan } from "@prisma/client"; export default async function Page(props: { searchParams?: Promise<{ @@ -72,9 +26,12 @@ export default async function Page(props: { }>; }) { const cookieStore = await cookies(); + const planID = await cookieStore.get("plan"); const pagePref = cookieStore.get("pagePref"); + if (pagePref && pagePref.value != "/") { redirect(pagePref.value); + } const searchParams = await props.searchParams; @@ -83,7 +40,8 @@ export default async function Page(props: { const dotw = searchParams?.dotw || []; const stime = searchParams?.stime || []; const homePageProps: any = {}; - const initalCourses = await getInitialCourses(); + const initalCourses = await getInitialCourses(query, term, dotw, stime); + const planCourses: CoursePlan[] = await getPlanCourses1(); homePageProps["fullCourseList"] = ( } > - + ); return ; // return with no events } async function Home(props: any) { - const terms = await getCourses(); + const terms = await getTerms(); const uniqueTimes = await getUniqueStartEndTimes(); - const codes = await getUniquCodes(); + const codes = await getUniqueCodes(); return ( <> @@ -134,7 +92,7 @@ async function Home(props: any) {
- + {props.createPlan}
diff --git a/components/FullCourseList.tsx b/components/FullCourseList.tsx index ae394b7..62e9172 100644 --- a/components/FullCourseList.tsx +++ b/components/FullCourseList.tsx @@ -38,7 +38,7 @@ export function FullCourseList({ setCursor((cursor) => cursor + NUMBER_OF_USERS_TO_FETCH); setTake((take) => take + NUMBER_OF_USERS_TO_FETCH); - const apiCourses = await getCourses(take, cursor, query, term, dotw, stime); + const apiCourses = await getCourses(take, query, term, dotw, stime); if ( inView && (apiCourses.length == 0 || apiCourses.length == courses.length) diff --git a/components/OLD.FullCourseList.tsx b/components/OLD.FullCourseList.tsx deleted file mode 100644 index ab2e7bc..0000000 --- a/components/OLD.FullCourseList.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { Course, Prisma } from "@prisma/client"; - -import prisma from "../lib/prisma"; - -import CourseCard from "./CourseCard"; -import React from "react"; - -async function getCourses( - query: string, - term: string, - dotw: Array, - stime: Array -) { - //let DOTW: Array = dotw.split(","); - - //query = query.trim().replace(/^[a-zA-Z0-9:]+$/g, ""); - - const startTime = stime.toString().split(",").filter(Number); - - return await prisma.course.findMany({ - relationLoadStrategy: "join", // or 'query' - include: { - sectionAttributes: true, - facultyMeet: { - include: { - meetingTimes: true, - }, - }, - instructor: true, - }, - - orderBy: [ - { - _relevance: { - fields: ["courseTitle", "subject", "courseNumber"], - search: query.trim().split(" ").join(" & "), - sort: "desc", - }, - }, - ], - where: { - ...(term - ? { - year: term, - } - : {}), - //year: term, - - ...(query - ? { - OR: [ - { - courseTitle: { - search: query.trim().split(" ").join(" | "), - mode: "insensitive", - }, - }, - { - sectionAttributes: { - some: { - code: { - search: query.trim().split(" ").join(" | "), - mode: "insensitive", - }, - }, - }, - }, - { - subject: { - search: query.trim().split(" ").join(" | "), - mode: "insensitive", - }, - }, - { - courseNumber: { - search: query.trim().split(" ").join(" | "), - mode: "insensitive", - }, - }, - { - instructor: { - displayName: { - search: query.trim().split(" ").join(" | "), - mode: "insensitive", - }, - }, - }, - ], - } - : {}), - - ...(startTime.length > 0 - ? { - facultyMeet: { - meetingTimes: { - beginTime: { - in: startTime, - }, - }, - }, - } - : {}), - - ...(dotw.length > 0 - ? { - facultyMeet: { - meetingTimes: { - is: { - monday: dotw.includes("monday") ? true : Prisma.skip, - tuesday: dotw.includes("tuesday") ? true : Prisma.skip, - wednesday: dotw.includes("wednesday") ? true : Prisma.skip, - thursday: dotw.includes("thursday") ? true : Prisma.skip, - friday: dotw.includes("friday") ? true : Prisma.skip, - saturday: dotw.includes("saturday") ? true : Prisma.skip, - sunday: dotw.includes("sunday") ? true : Prisma.skip, - }, - }, - }, - } - : {}), - }, - }); -} - -export async function FullCourseList({ - query, - term, - dotw, - stime, -}: { - query: string; - term: string; - dotw: Array; - stime: Array; -}) { - const courseList: Course[] = await getCourses(query, term, dotw, stime); - - return ( - <> -
- {courseList?.map((course: any) => ( -
- -
- ))} -
- - ); -} diff --git a/components/PlanContext.tsx b/components/PlanContext.tsx new file mode 100644 index 0000000..2119847 --- /dev/null +++ b/components/PlanContext.tsx @@ -0,0 +1,24 @@ +"use client"; +import { createContext, useContext, useState } from "react"; +import { CoursePlan } from "@prisma/client"; +import { getInitialCoursePlans } from "../app/actions/getCourses"; + +const PlanContext = createContext([]); + +export const PlanContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [coursePlans, setCoursePlans] = useState([]); + + return ( + + {children} + + ); +}; + +export const usePlanContext = () => { + return useContext(PlanContext); +}; diff --git a/components/Search.tsx b/components/Search.tsx index 4087c90..c0633cd 100644 --- a/components/Search.tsx +++ b/components/Search.tsx @@ -51,7 +51,7 @@ export default function Search(props: any) { params.delete("query"); } replace(`${pathname}?${params.toString()}`); - }); + }, 100); const handleSelectionChange = (e: any) => { setSelectedTerm([e.target.value]); @@ -85,6 +85,7 @@ export default function Search(props: any) { setSelectedDOTW(searchParams.get("dotw")?.toString().split(",")); setSelectedStartTime(searchParams.get("stime")?.toString().split(",")); //handleSelectionChange({ target: { value: selectedTerm } }); + }, [searchParams]); useEffect(() => { diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml index a389da7..1a94fa4 100644 --- a/docker-compose.debug.yml +++ b/docker-compose.debug.yml @@ -1,5 +1,6 @@ services: - schedulerv2: + planner-dev: + container_name: planner-dev build: context: . dockerfile: ./Dockerfile.dev @@ -8,8 +9,7 @@ services: environment: NODE_ENV: development DOMAIN: http://127.0.0.1:3000/ - DATABASE_URL: "postgresql://postgres:example@postgres:5432/scheduler_db" - POSTGRES_DB: scheduler_db + POSTGRES_DB: planner_db env_file: - .env ports: @@ -19,13 +19,14 @@ services: - internal command: sh -c "npm install --silent && npx prisma migrate dev --name init && npx prisma generate && npm run dev " - postgres: + planner-db-dev: + container_name: planner-db-dev image: postgres:16.4-bullseye ports: - 5432:5432 environment: POSTGRES_PASSWORD: example - POSTGRES_DB: scheduler_db + POSTGRES_DB: planner_db networks: - internal volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 341ea2c..887f8ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: - scheduler: - image: registry.sccs.swarthmore.edu/sccs/scheduler/scheduler:latest + planner: + image: registry.sccs.swarthmore.edu/sccs/planner/planner:latest build: context: . dockerfile: ./Dockerfile @@ -9,28 +9,28 @@ services: - .env environment: NODE_ENV: production - DOMAIN: https://schedulerv2.sccs.swarthmore.edu + DOMAIN: https://plan.sccs.swarthmore.edu depends_on: - - scheduler-db + - planner-db deploy: labels: - - 'traefik.enable=true' - - 'traefik.docker.network=traefik' - - 'traefik.http.routers.scheduler.entrypoints=https' - - 'traefik.http.routers.scheduler.rule=Host(`schedulerv2.sccs.swarthmore.edu`)' - - 'traefik.http.routers.scheduler.tls=true' - - 'traefik.http.routers.scheduler.tls.certresolver=letsEncrypt' - - 'traefik.http.services.scheduler.loadbalancer.server.port=3000' + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.http.routers.planner.entrypoints=https" + - "traefik.http.routers.planner.rule=Host(`plan.sccs.swarthmore.edu`)" + - "traefik.http.routers.planner.tls=true" + - "traefik.http.routers.planner.tls.certresolver=letsEncrypt" + - "traefik.http.services.planner.loadbalancer.server.port=3000" command: sh -c "sleep 5 && npx prisma migrate deploy && npm start " networks: - traefik - internal - scheduler-db: - hostname: scheduler-db + planner-db: + hostname: planner-db image: postgres:16.4-bullseye volumes: - - scheduler-dbdata:/var/lib/postgresql/data + - planner-dbdata:/var/lib/postgresql/data env_file: - .env ports: @@ -38,8 +38,8 @@ services: networks: - internal - scheduler-cron: - image: registry.sccs.swarthmore.edu/sccs/scheduler/scheduler-cron:latest + planner-cron: + image: registry.sccs.swarthmore.edu/sccs/planner/planner-cron:latest build: context: . dockerfile: ./Dockerfile.cron @@ -47,7 +47,7 @@ services: env_file: - .env depends_on: - - scheduler-db + - planner-db networks: - internal @@ -60,9 +60,9 @@ networks: external: true volumes: - scheduler-dbdata: - name: scheduler-dbdata + planner-dbdata: + name: planner-dbdata driver_opts: type: nfs o: "nfsvers=4,addr=130.58.218.26,rw,nolock,soft" - device: ":/volumes/scheduler-dbdata" + device: ":/volumes/planner-dbdata" diff --git a/public/logo/logo.png b/public/logo/logo.png new file mode 100644 index 0000000..22b8179 Binary files /dev/null and b/public/logo/logo.png differ