# Project export: Slug-Schedule

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2025
- Tagline: Next quarter's perfect course scheduler, minutes away
- Devpost: https://devpost.com/software/slug-schedule
- GitHub: https://github.com/charlesphu/slugscheduler
- Demo: https://slug-scheduler.vercel.app/home
- Team: 4 GitHub contributor(s) — Dino (70 commits), charlesphu (34 commits), Ala'addin Ghosheh (12 commits), asvinod (8 commits)

## Devpost submission (written by the team)

### Inspiration

Enrolling for classes at UCSC can be tedious and unintuitive. For instance, checking class eligibility or identifying scheduling conflicts often requires extra steps, making the process frustrating. We wanted to create a user-friendly and intuitive application that simplifies scheduling and enhances the overall experience for students.

### What it does

Slug Scheduler analyzes your transcript and recommends classes to take for the next quarter based on the current course offerings.

### How we built it

We used Next.js as the main framework and designed the interface using Figma. The project was powered by JavaScript, the npm package manager, and the GEMINI API for transcript analysis.

### Challenges we ran into

Time management and task prioritization were significant challenges. We underestimated the complexity of certain tasks, which slowed down progress. Additionally, maintaining consistent productivity over extended periods proved difficult as fatigue set in.

### Accomplishments we're proud of

We successfully built a web scraper capable of gathering comprehensive course data for a specific quarter. This includes course names, timings, enrollment numbers, and available slots, providing students with up-to-date and accurate information.

### What we learned

We learned the importance of researching and familiarizing ourselves with the technologies we plan to use beforehand. For example, not deciding on a component library or CSS framework early on led to inefficiencies, with only one team member handling the frontend.

### What's next

for Slug Scheduler We aim to add features like exporting schedules to Google Calendar for improved usability. Additionally, we plan to integrate Rate My Professor ratings to recommend courses based on professor reviews, helping students make more informed decisions.

## README (from the GitHub repository)

# Slug Scheduler

A smart scheduling assistant for UC Santa Cruz students that creates recommended class schedules in minutes. Upload your transcript to analyze academic progress, generate schedules based on major requirements, and visualize your courses in an interactive calendar interface!

## 🎯 Features

- **PDF Transcript Analysis**: Upload your academic transcript to automatically analyze your academic progress
- **Smart Schedule Generation**: Creates optimized class schedules based on your major requirements
- **Interactive Calendar**: Visualize and modify your class schedule with an intuitive drag-and-drop interface
- **Major Requirements Integration**: Automatically checks degree requirements against UCSC's course catalog
- **Web Scraping**: Pulls course data from UCSC's class search and major websites

## 📸 Screenshots

![Home Page](public/screenshots/Home.png)
_Friendly landing page for users to upload their university transcript_

![Calendar View](public/screenshots/Schedule.png)
_Calendar view with course recommendations for easy scheduling_

## 🚀 Tech Stack

- **Framework**: [Next.js 15.3.0](https://nextjs.org/)
- **UI Components**:
  - [DND Kit](https://dndkit.com/) for drag-and-drop functionality
  - [Tailwind CSS](https://tailwindcss.com/) for styling
- **AI Integration**: Google's [Gemini AI API](https://ai.google.dev/) for transcript analysis
- **PDF Processing**: [PDF.js](https://mozilla.github.io/pdf.js/) for transcript parsing

## 🛠️ Installation

1. Clone the repository:

```bash
git clone https://github.com/charlesphu/slugscheduler.git
```

2. Install dependencies:

```bash
npm install
```

3. Create a `.env.local` file and add your Gemini API key:

```bash
GEMINI_API_KEY="your-api-key"
```

4. Start the development server:

```bash
npm run dev
```

5. Open [http://localhost:3000](http://localhost:3000) in your browser

## 💻 Development

- `npm run dev`: Starts development server with Turbopack
- `npm run build`: Creates production build

## 🙏 Acknowledgments

### The Team:

- **Andy Kuang** - Frontend & UX Design
- **Charles Phu** - Backend & Webscraper
- **Ashwin Vinod** - Backend & Gemini
- **Alaaddin Ghosheh** - Backend & Gemini

Special thanks to the CruzHacks 2025 organizing team for hosting an amazing hackathon that made this project possible

## 🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


## Detected evidence (automated analysis)

Indexed codebase: 25 recognized source files, 48 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (34 of 34)

```
.gitignore
.prettierrc
.vscode/settings.json
app/api/classes/route.js
app/api/gemini/route.js
app/api/gemini/utils/geminiHelper.js
app/api/timeScraper/route.js
app/api/timeScraper/utils/classhelper.js
app/api/timeScraper/utils/scraper.js
app/calendar/ClassOptions.js
app/calendar/ClassSchedule.js
app/calendar/Container.js
app/calendar/CourseItem.js
app/calendar/page.js
app/calendar/Title.js
app/globals.css
app/home/page.js
app/home/Title.js
app/home/UploadForm.js
app/hooks/aiPrompt.js
app/hooks/fetchRemainingClasses.js
app/hooks/suggestRecs.js
app/hooks/usePDFToText.js
app/hooks/webscrape.js
app/layout.js
app/metadata.js
eslint.config.mjs
jsconfig.json
next.config.mjs
package.json
postcss.config.mjs
public/cseClasses.json
README.md
utils/format.js
```

### Dependencies

- package.json: @dnd-kit/core@^6.3.1, @eslint/eslintrc@^3, @fullcalendar/core@^6.1.17, @fullcalendar/daygrid@^6.1.17, @fullcalendar/interaction@^6.1.17, @fullcalendar/react@^6.1.17, @fullcalendar/timegrid@^6.1.17, @google/genai@^0.8.0, @tailwindcss/postcss@^4.1.3, axios@^1.8.4, canvas@^3.1.0, cheerio@^1.0.0, eslint@^9, eslint-config-next@15.3.0, fs@^0.0.1-security, head@^1.0.0, next@15.3.0, pdfjs-dist@^3.11.174, postcss@^8.5.3, prettier-plugin-tailwindcss@^0.6.11, react@^19.0.0, react-dom@^19.0.0, react-dropzone@^14.3.8, react-scan@^0.3.3, tailwind-merge@^3.2.0, tailwindcss@^4.1.3

### Recent commits (newest first)

- remove console.logs
- Add loading state to ClassOptions and integrate with CalendarPage
- Update course recommendation logic to dynamically mark first four classes as recommended
- Merge branch 'main' of https://github.com/charlesphu/slugscheduler
- suggestions working
- Update README.md
- Reorder contribution README
- Update readme acknowledgements
- Update README description
- Update README.md
- Add new screenshots
- Refactor course filtering logic to shift items up when removed from options
- Hide courses marked as hidden when dragged and update event handling logic
- Remove underscore from container
- Update container styles to use tertiary background color
- Merge branch 'main' of github.com:charlesphu/slugschedule
- Fix event callback dependencies
- Update README.md
- Delete screenshots directory
- Merge branch 'main' of github.com:charlesphu/slugschedule

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "slugschedule",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@dnd-kit/core": "^6.3.1",
    "@fullcalendar/core": "^6.1.17",
    "@fullcalendar/daygrid": "^6.1.17",
    "@fullcalendar/interaction": "^6.1.17",
    "@fullcalendar/react": "^6.1.17",
    "@fullcalendar/timegrid": "^6.1.17",
    "@google/genai": "^0.8.0",
    "axios": "^1.8.4",
    "canvas": "^3.1.0",
    "cheerio": "^1.0.0",
    "fs": "^0.0.1-security",
    "head": "^1.0.0",
    "next": "15.3.0",
    "pdfjs-dist": "^3.11.174",
    "postcss": "^8.5.3",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-dropzone": "^14.3.8",
    "react-scan": "^0.3.3",
    "tailwind-merge": "^3.2.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@tailwindcss/postcss": "^4.1.3",
    "eslint": "^9",
    "eslint-config-next": "15.3.0",
    "prettier-plugin-tailwindcss": "^0.6.11",
    "tailwindcss": "^4.1.3"
  }
}

```

### app/layout.js

```javascript
"use client";
import "./globals.css";

import { Geist, Geist_Mono } from "next/font/google";
import React, { createContext, useState } from "react";
import Script from "next/script";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const UserDataContext = createContext();

export default function RootLayout({ children }) {
  const [transcriptData, setTranscriptData] = useState(null); // State for transcript data

  return (
    <html lang="en">
      <head>
        {/* Only include React-Scan in development */}
        {/* {process.env.NODE_ENV === "development" && (
          <Script
            crossOrigin="anonymous"
            src="//unpkg.com/react-scan/dist/auto.global.js"
          />
        )} */}
      </head>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        <UserDataContext.Provider value={{ transcriptData, setTranscriptData }}>
          {children}
        </UserDataContext.Provider>
      </body>
    </html>
  );
}

```

### app/home/page.js

```javascript
import React from "react";

import Title from "./Title";
import UploadForm from "./UploadForm";

export default function Home() {
  return (
    <div>
      {/* Content */}
      <div className="relative z-1 flex h-screen w-full items-center justify-between p-20">
        <Title />
        <UploadForm />
      </div>

      {/* Stickers and Decorations */}
      <div className="pointer-event-none absolute top-0 h-screen w-full select-none">
        <img
          className="absolute top-5 left-20 h-[20%] w-[12%] object-contain"
          src={"Decor 1.png"}
          alt="Decoration"
          draggable={false}
        />
        <img
          className="absolute top-2 right-20 h-[20%] w-[7%] object-contain"
          src={"Decor 2.png"}
          alt="Decoration"
          draggable={false}
        />
        <img
          className="absolute bottom-2 left-20 z-3 h-[30%] w-[20%] object-contain"
          src={"Slug Draw.png"}
          alt="Slug Drawing"
          draggable={false}
        />
        <img
          className="absolute right-20 bottom-2 z-3 h-[30%] w-[20%] object-contain"
          src={"Decor 3.png"}
          alt="Decoration"
          draggable={false}
        />
      </div>
    </div>
  );
}

```

### app/calendar/page.js

```javascript
"use client";

import { useCallback, useContext, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useState } from "react";

import { UserDataContext } from "../layout";

import fetchRemainingClasses from "../hooks/fetchRemainingClasses";
import { suggestRecs } from "../hooks/suggestRecs";

import { DndContext } from "@dnd-kit/core";

import Title from "./Title";

import ClassOptions from "./ClassOptions";
import ClassSchedule from "./ClassSchedule";

function createEventsFromCourse(courseData) {
  const { code, name, schedule } = courseData;
  const { dayAndTime, location } = schedule;

  // Skip if no specific time
  if (dayAndTime === "Not specified") {
    return [];
  }

  // Parse day and time information
  // Example format: "TuTh 08:25AM-10:00AM" or "M 12:00PM-03:00PM"
  const dayMatch = dayAndTime.match(
    /^([MTuWThF]+)\s+(\d+:\d+(?:AM|PM))-(\d+:\d+(?:AM|PM))/
  );

  if (!dayMatch) {
    return [];
  }

  const dayStr = dayMatch[1];
  const startTime = dayMatch[2];
  const endTime = dayMatch[3];

  // Convert days string to array of day codes
  const dayMapping = {
    M: "MO",
    Tu: "TU",
    W: "WE",
    Th: "TH",
    F: "FR",
  };

  const days = [];
  let current = "";

  // Parse the day string (e.g., "TuTh" -> ["TU", "TH"])
  for (let i = 0; i < dayStr.length; i++) {
    current += dayStr[i];
    if (dayMapping[current]) {
      days.push(dayMapping[current]);
      current = "";
    }
  }

  // Create an event for each day
  const events = [];
  const baseDate = "2025-04-14"; // Monday
  const dayOffsets = { MO: 0, TU: 1, WE: 2, TH: 3, FR: 4 };

  // Convert time to 24-hour format
  const convertTo24Hour = (timeStr) => {
    const [time, modifier] = timeStr.split(/(?:AM|PM)/);
    let [hours, minutes] = time.split(":");
    hours = parseInt(hours, 10);

    if (timeStr.includes("PM") && hours < 12) {
      hours += 12;
    }
    if (timeStr.includes("AM") && hours === 12) {
      hours = 0;
    }

    return `${hours.toString().padStart(2, "0")}:${minutes}`;
  };

  const start24 = convertTo24Hour(startTime);
  const end24 = convertTo24Hour(endTime);

  days.forEach((day) => {
    const dayOffset = dayOffsets[day];
    const eventDate = new Date(baseDate);
    eventDate.setDate(eventDate.getDate() + dayOffset);

    const dateStr = eventDate.toISOString().split("T")[0];

    events.push({
      title: `${code} - ${name}`,
      start: `${dateStr}T${start24}:00`,
      end: `${dateStr}T${end24}:00`,
      backgroundColor: generateColorFromString(code),
      extendedProps: {
        location: location,
        instructionMode: schedule.instructionMode,
        courseCode: code,
      },
      editable: false,
      draggable: false,
      startEditable: false,
      durationEditable: false,
    });
  });

  return events;
}

// Generate a consistent color based on the course code
function generateColorFromString(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }

  const h = hash % 360;
  return `hsl(${h}, 70%, 70%)`;
}

export default function CalendarPage() {
  const [events, setEvents] = useState([]);
  const [remainingClasses, setRemainingClasses] = useState([]);

  const userDataContext = useContext(UserDataContext);
  const router = useRouter();

  const [isLoadingRecommendation, setIsLoadingRecommendation] = useState(false);

  useEffect(() => {
    if (
      !userDataContext ||
      !userDataContext.transcriptData ||
      !userDataContext.transcriptData.classes
    ) {
      router.push("/");
    } else {
      setIsLoadingRecommendation(true);
      fetchRemainingClasses(userDataContext.transcriptData.classes)
        .then((res) => {
          // setRemainingClasses(res);
          suggestRecs(res).then((classes) => {
            const rawText = classes.data.candidates[0].content.parts[0].text;
            const cleanedText = rawText.replace(/```json|```/g, "").trim();
            try {
              // Mark the first four classes as recommended
              const parsedData = JSON.parse(cleanedText);
              parsedData.forEach((course, index) => {
                if (index < 4) {
                  course.recommended = true;
                }
              });

              setIsLoadingRecommendation(false);
              setRemainingClasses(parsedData);
            } catch (error) {
              console.error(
                "Error parsing JSON:",
                error,
                "Cleaned text:",
                cleanedText
              );
            }
          });
        })
        .catch((err) => {
          console.error("Error fetching remaining classes:", err);
          setRemainingClasses([]);
        });
    }
  }, [userDataContext]);

  const handleDragEnd = useCallback(
    (event) => {
      const { active, over } = event;

      if (over && over.id === "calendar-drop-area") {
        // Find the course data based on the dragged item ID (courseName)
        const courseData = remainingClasses.find(
          (course) => course.code === active.id
        );

        if (courseData) {
          // Add the course as a new calendar event
          addCourseToCalendar(courseData);
        }
      }
    },
    [remainingClasses]
  );

  const addCourseToCalendar = useCallback((courseData) => {
    // Parse the day and time information from course schedule
    const { schedule } = courseData;
    if (
      schedule.dayAndTime === "Not specified" ||
      schedule.instructionMode === "Asynchronous Online"
    ) {
      // Handle async courses or those without specific times
      alert(`${courseData.code} is asynchronous or has no specified time`);
      return;
    }

    // Create calendar events from the course data
    let newEvents = createEventsFromCourse(courseData);

    // Update events and make sure the new events are unique
    setEvents((prev) => {
      const existingCourseCodes = new Set(
        prev.map((event) => e
[truncated — 2527 more characters]
```

### app/api/timeScraper/route.js

```javascript
import { getClasses } from "./utils/scraper.js";

export async function GET(request) {
  try {
    // Call the scraping function
    const result = await getClasses();

    // Optionally save to a file (if needed)
    // const fs = require("fs");
    // fs.writeFileSync('classes.json', JSON.stringify(result, null, 2));

    return new Response(JSON.stringify({ success: true, data: result }), {
      status: 200,
    });
  } catch (error) {
    return new Response(
      JSON.stringify({ success: false, message: error.message }),
      { status: 500 }
    );
  }
}

```

### app/api/gemini/route.js

```javascript
import { sendTxtAndPrompt } from "./utils/geminiHelper";

export async function POST(request) {
  try {
    const { requestType, content } = await request.json();
    let prompt = null;

    if (requestType == "pdf") {
      if (!content) {
        return new Response(
          JSON.stringify({ success: false, error: "PDF text is required." }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }
      prompt =
        "This is a pdf that is the transcript of a student. " +
        "Please parse the pdf for all of the classes that have been taken by a user, " +
        "and return it in your response as an array. Your response is being returned as a json object " +
        "so please be mindful of this and do not create any sort of disruptive writing or formatting. " +
        "I just need your response as an array of taken class codes (e.g., CSE20, BIO120, etc.) as a comma-separated list, " +
        "and some additional student information. Structure your response like this as json data: " +
        "classes: (class array)" +
        "GPA: (Student GPA) " +
        "Total Units: (total units) " +
        "Current year: (student year, such as freshman, sophomore, junior, etc. Determine by timeline evident in transcript)" +
        "Current Major: (student major, please make it read and casual, no shorthand language. Like: Computer Science, Biology) " +
        "Short words: (max 20 words) of encouragement based off recently taken classes: (Encouragement)" +
        content;
    } else if (requestType == "majorRequirements") {
      if (!content) {
        return new Response(
          JSON.stringify({ success: false, error: "URL text is required." }),
          { status: 400, headers: { "Content-Type": "application/json" } }
        );
      }
      prompt =
        "This is a parsed link that contains all the major qualification courses for the student. " +
        "Return the courses codes and their course names as an array. Your response is being retruend as a json object, " +
        "so please be mindful of this and do not create any sort of disruptive writing or formatting. " +
        "I just need your response as an array of taken class codes (e.g., CSE20, BIO120, etc.) as a comma-separated list, " +
        "and some additional class information, like credit number and the number of electives a student needs to take. " +
        "Along with that, I want you to make the list of the classes that are ELECTIVES in an array [ ]. Here is my structure idea: " +
        "All classes = [all classes ]" +
        "courses: { class: {code, name, credits} class: {code, name, credits}.... number of electives to take: (number) electives: (electives array) }" +
        content;
    } else if (requestType == "rankings") {
      prompt =
        /*  "This is JSON data for classes that are being offered in the current academic quarter that a student is able to take " +
        "Please reearange the JSON data in order from highest to lowest ranking." +
        "please be mindful that this response will be treated as a JSON object so do not return anything other than a JSON object. Do not drop any existing attributes such as class schedule, every index must have them. Please add an attribute for the top 5 positions to imply it is a 'reccomended' course. such as reccomended: true, only" +
        "for the top 5 classes. Here is the JSON data. " +
        "Do not add any additional information or context to the response. " +
        "Do not create hypothetical classes or any other information" +
        "You must just return the JSON data provided rearranged with the new attribute for the top 5."
        "Only return values from the existing JSON data. Structure the JSON as such:" +
        "Code: string"+
        "Time: (such as TuTh 10:00-11:30)"+
        "name: string"+ **/
        "This is the JSON data for classes that are being offered; " +
        JSON.stringify(content) +
        "Please rearrange this data in order from highest to lowest ranking." +
        "You must just return the JSON data provided rearranged with the new attribute for the top 5.";
      "Only return values from the existing JSON data. Structure the JSON as such:" +
        "Code: string" +
        "Time: (such as TuTh 10:00-11:30)" +
        "name: string";
    }
    const geminiResponse = await sendTxtAndPrompt(prompt);

    return new Response(JSON.stringify(geminiResponse), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  } catch (error) {
    console.error("Error in route.js:", error);
    return new Response(
      JSON.stringify({
        success: false,
        error: "Failed to process the request.",
        details: error.message,
      }),
      { status: 500, headers: { "Content-Type": "application/json" } }
    );
  }
}

```

### app/api/classes/route.js

```javascript
import fetch from "node-fetch";
import * as cheerio from "cheerio";
import { writeFileSync } from "fs";

const ENDPOINT = "https://pisa.ucsc.edu/cs9/prd/sr9_2013/index.php";
const RESULTS_PER_PAGE = "2500"; // Number of results per page
async function fetchPage(startAt) {
  const formData = new URLSearchParams({
    action: "results",
    rec_dur: RESULTS_PER_PAGE, // Keep at 25 as this is what the system expects
    rec_start: startAt.toString(), // Convert to string for URLSearchParams
    "binds[:term]": "2252",
    "binds[:reg_status]": "all",
    "binds[:subject]": "",
    "binds[:catalog_nbr_op]": "=",
    "binds[:catalog_nbr]": "",
    "binds[:title]": "",
    "binds[:instr_name_op]": "=",
    "binds[:instructor]": "",
    "binds[:ge]": "",
    "binds[:crse_units_op]": "=",
    "binds[:crse_units_from]": "",
    "binds[:crse_units_to]": "",
    "binds[:crse_units_exact]": "",
    "binds[:days]": "",
    "binds[:times]": "",
    "binds[:acad_career]": "",
  });

  try {
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent":
          "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36",
      },
      body: formData,
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return await response.text();
  } catch (error) {
    console.error("Error fetching page:", error);
    throw error;
  }
}

async function getClasses() {
  try {
    const classes = [];
    let startAt = 1;
    let hasMoreResults = true;

    while (hasMoreResults) {
      // Add delay between requests
      if (startAt > 1) {
        await new Promise((resolve) => setTimeout(resolve, 1500));
      }

      console.log(`Fetching courses ${startAt} to ${startAt + 24}...`);

      const html = await fetchPage(startAt);
      const $ = cheerio.load(html);

      const courseElements = $(".panel.panel-default.row");
      console.log("Course elements found:", courseElements.length);

      // Add logging to track progress
      console.log(`Current page results: ${courseElements.length}`);
      console.log(`Total results found so far: ${classes.length}`);
      console.log(`Has more results: ${hasMoreResults}`);

      if (courseElements.length === 0) {
        console.log("No more courses found.");
        hasMoreResults = false;
        continue;
      }

      courseElements.each((_, el) => {
        const find = (selector) => $(el).find(selector);

        // Extract code and name
        const titleElement = find("h2 a");
        const titleText = titleElement.text().trim();
        const [code, ...nameParts] = titleText.split(/\u00A0\u00A0\u00A0/);
        const name = nameParts.join(" ").trim();

        if (!code || !name) return;

        // Extract day and time information
        const dayAndTime = find(
          ".panel-body .row > div:nth-child(3) > div:nth-child(2)"
        )
          .text()
          .replace("Day and Time:", "")
          .trim();

        // Extract location
        const location = find(
          ".panel-body .row > div:nth-child(3) > div:nth-child(1)"
        )
          .text()
          .replace("Location:", "")
          .trim();

        // Extract instruction mode
        const instructionMode = find(".hide-print b").text().trim();

        // Extract enrollment info
        // const enrollmentText = find(".panel-body .row > div:nth-child(4)")
        //   .text()
        //   .trim();
        // let enrolled = 0,
        //   capacity = 0;
        // const enrollmentMatch = enrollmentText.match(/(\d+) of (\d+)/);
        // if (enrollmentMatch) {
        //   enrolled = parseInt(enrollmentMatch[1]);
        //   capacity = parseInt(enrollmentMatch[2]);
        // }

        classes.push({
          code: code.trim(),
          name,
          schedule: {
            dayAndTime: dayAndTime || "Not specified",
            location: location || "Not specified",
            instructionMode: instructionMode || "Not specified",
          },
          // enrollment: {
          //   enrolled,
          //   capacity,
          // },
        });
      });

      // Check if there's a next page by looking at the bottom navigation
      const resultsInfo = $('td[align="right"]').text().trim();
      const totalResults = parseInt(resultsInfo.match(/of (\d+)/)?.[1] || "0");
      console.log(`Total results: ${totalResults}`);
      if (startAt + courseElements.length >= totalResults) {
        hasMoreResults = false;
        console.log("Reached the end of results.");
      } else {
        startAt += RESULTS_PER_PAGE; // Move to next page
      }

      console.log(
        `Found ${courseElements.length} courses on this page. Total so far: ${classes.length}`
      );
    }

    // Add metadata to the final output
    const output = {
      metadata: {
        totalClasses: classes.length,
      },
      classes: classes,
    };

    return output;
  } catch (error) {
    console.error("Error in getClasses:", error);
    throw error;
  }
}

// Run the scraper

getClasses()
  .then((output) => {
    // Pretty print the results
    console.log("\nFound classes:");
    // output.classes.forEach((cls) => {
    //   console.log("\n-------------------");
    //   console.log(`Course: ${cls.code}`);
    //   console.log(`Name: ${cls.name}`);
    //   console.log(`Time: ${cls.schedule.dayAndTime}`);
    //   console.log(`Location: ${cls.schedule.location}`);
    //   console.log(`Mode: ${cls.schedule.instructionMode}`);
    //   console.log(
    //     `Enrollment: ${cls.enrollment.enrolled}/${cls.enrollment.capacity}`
    //   );
    // });
    console.log("\n-------------------");
    console.log(`Total number of classes: ${output.classes.length}`);

    // Save to a file with metadata
    writeFileSync("classes.json", JSON.stringify(output, null, 2));
    console
[truncated — 129 more characters]
```

### utils/format.js

```javascript


```

### app/metadata.js

```javascript
export const metadata = {
    title: "Create Next App",
    description: "Generated by create next app",
  };
```

### app/globals.css

```css
@import "tailwindcss";

:root {
  --background: #eaedcc;
  --container-primary: #fdfae2;
  --container-secondary: #f8edd0;
  --text-primary: #cca57a;

  --fc-border-color: var(
    --text-primary
  ); /* Override FullCalendar border color */
}

body {
  background: var(--background);
  font-family: "Cabin", sans-serif;
}

/* Removes title from FullCalender component */
.fc .fc-toolbar {
  display: none;
}

/* Set custom styles */
.fc {
  font-family: "Cabin", sans-serif;
}

.fc .fc-timegrid-slot {
  color: var(--text-primary) !important;
  border: 1px dashed var(--container-secondary) !important;
}

.fc .fc-col-header-cell {
  color: var(--text-primary) !important;
  border: 1px solid var(--text-primary) !important;
}

.fc .fc-scrollgrid {
  border-radius: 5px;
}

```

[15 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]