# Project export: UCSC Info

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: An all-in-one UCSC website with a news feed, course search, dining hall menu, and AI.
- Devpost: https://devpost.com/software/ucsc-info
- GitHub: https://github.com/Seanathan10/CruzHacks2025
- Demo: http://ucscinfo.tech/
- Team: 4 GitHub contributor(s) — Sean Singh (41 commits), iconsumeplutonium (35 commits), Ch4xm (21 commits), Bill Zhang (14 commits)

## Devpost submission (written by the team)

### Inspiration

As students at UCSC, we often need quick access to a variety of information, such as dining hall menus and next quarter’s class offerings. Unfortunately, this information is spread out and buried across multiple websites, most of which are poorly designed for mobile use. To solve this, we set out to build an all-in-one website that’s cross-platform, user-friendly, and easily accessible on both desktop and mobile devices.

### What it does

Our site features a unified newsfeed that aggregates news from various departments into a single, consistent stream for easy browsing with the ability to filter news by department. The dining hall menu page compiles meal information from all UCSC dining locations, presenting it in a clear and convenient layout. Students can also search for courses across different quarters and view detailed class information. Additionally, we’ve included a personalized briefing tab on the dashboard, powered by Google Gemini, to provide users with summaries of on the latest happenings at UCSC. The entire site is fully responsive, ensuring a smooth experience on both desktop and mobile devices. We've also added a site-wide dark/light mode toggle for customizable viewing.

### How we built it

We did not use any UI libraries and rolled all our CSS ourselves. Our backend, written in Python, scrapes and processes the data off UCSC's websites and serves it as an API to our frontend, which was written in React. We did not use any UI libraries for this project and rolled all our UI ourselves.

### Challenges we ran into

One of the biggest challenges we ran into for this project is the responsive design. From the start, we wanted to ensure cross-platform accessibility on the site and we had to spent a significant amount of time attempting to reduce create a good, scalable UI that is clean and fast.

### Accomplishments we're proud of

We are incredibly proud of our responsive design on our webpage and its cross-platform usability. We are also incredibly proud that we didn't use any UI frameworks and managed to create a good looking UI from scratch all on our own. We are also incredibly proud that we managed to build these tools in under 48 hours.

### What we learned

We learned a lot about good website design and how to design responsive and user-friendly interfaces. We learned how to create layouts that adapt seamlessly across different screen sizes and devices, ensuring a consistent and accessible user experience on both desktop and mobile. We also learned how to create a robust backend API that is both fast and reliable.

## README (from the GitHub repository)

# UCSC.app

Born out of a Cruzhacks 2025 project, UCSC.app is a responsive, full-stack application for students that aggregates campus dining menus, news feeds, course information, events, and location data to help UCSC students navigate campus life efficiently.

## Features

- Class search
  - Add classes to your calendar with one click
- Dining Hall Menus
- News Feed
- Schedule Map
	- Interactive campus map that shows what classes take place in a building

## Getting Started

### Prerequisites
- Node.js and pnpm
- Python 3.8+ and uv

### Frontend Setup

1. Navigate to the project root and install dependencies:
```bash
pnpm install
```

2. Start the development server:
```bash
pnpm run dev
```

3. Build for production:
```bash
pnpm build
```

### Backend Setup

1. Navigate to the backend directory:
```bash
cd backend
```

2. Create a Python virtual environment:
```bash
uv sync
```

3. Run the development server:
```bash
uv run uvicorn server:api --reload --host 0.0.0.0
```

## Project Structure

```
ucsc.app/
├── src/                # Frontend React application
│   ├── common/           # Reusable UI components for all pages
│   ├── courses/          # Course search and display
│   ├── dashboard/        # Dashboard page
│   ├── menu/             # Dining menu pages
│   ├── news/             # News feed pages
│   ├── schedules/        # Schedule and map pages
└─ backend/             # FastAPI backend
   ├── endpoints/         # API route handlers
   ├── locations/         # Location and building data
   └── cache/             # Cached data
```

## Detected evidence (automated analysis)

Indexed codebase: 100 recognized source files, 289 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (115 of 115)

```
.github/workflows/deploy.yml
.gitignore
.vscode/launch.json
backend/.python-version
backend/cache/cron.py
backend/endpoints/bigbrother.py
backend/endpoints/courses.py
backend/endpoints/menu.py
backend/endpoints/news.py
backend/locations/create.sql
backend/locations/locations.py
backend/locations/locationscraper.py
backend/locations/loctypes.py
backend/locations/pisadownloader.py
backend/locations/readbin.py
backend/locations/README.md
backend/pyproject.toml
backend/requirements.txt
backend/server.py
backend/uv.lock
eslint.config.js
index.html
package.json
public/404.html
public/manifest.json
public/robots.txt
public/s.js
public/sitemap.xml
README.md
src/App.css
src/App.tsx
src/common/constants.ts
src/common/Icon.tsx
src/common/loading/Loading.css
src/common/loading/Loading.tsx
src/common/navbar/desktop/TopBar.css
src/common/navbar/desktop/TopBar.tsx
src/common/navbar/mobile/TopBar.css
src/common/navbar/mobile/TopBar.tsx
src/common/navbar/NavbarButtons.tsx
src/common/seo/PageMeta.tsx
src/common/seo/routeMeta.ts
src/common/seo/schema.ts
src/common/terms.ts
src/common/theme/ThemeChanger.css
src/common/theme/ThemeChanger.tsx
src/Context.tsx
src/courses/Card.tsx
src/courses/components/Buttons.tsx
src/courses/components/ClassDetails.tsx
src/courses/components/ClassMeetings.tsx
src/courses/components/ClassSections.tsx
src/courses/components/MeetingInfo.tsx
src/courses/components/Section.tsx
src/courses/Courses.tsx
src/courses/CourseSearchPanel.tsx
src/courses/DetailedView.tsx
src/courses/Filters.tsx
src/courses/generateIcs.ts
src/courses/Search.tsx
src/courses/StatusEmoji.tsx
src/courses/styles/Card.css
src/courses/styles/Courses.css
src/courses/styles/DetailedView.css
src/courses/styles/Filters.css
src/courses/styles/Search.css
src/dashboard/Dashboard.css
src/dashboard/Dashboard.tsx
src/declarations.d.ts
src/index.css
src/main.tsx
src/menu/api.ts
src/menu/DateHeader.tsx
src/menu/desktop/DateHeader.tsx
src/menu/desktop/DateSelector.css
src/menu/desktop/DateSelector.tsx
src/menu/desktop/Menu.tsx
src/menu/FoodBlock.tsx
src/menu/MealHeader.tsx
src/menu/Menu.css
src/menu/MenuPage.tsx
src/menu/MenuPanel.tsx
src/menu/mobile/DateSelector.css
src/menu/mobile/Menu.tsx
src/news/News.css
src/news/News.tsx
src/news/NewsCard.tsx
src/news/NewsFilter.tsx
src/news/NewsSidebar.tsx
src/news/Tag.tsx
src/schedules/BuildingPopup.tsx
src/schedules/components/BuildingRoomList.tsx
src/schedules/components/DaySelector.tsx
src/schedules/components/ScheduleTimeBlock.tsx
src/schedules/data/buildingLookup.json
src/schedules/data/convert.js
src/schedules/data/filtered.json
src/schedules/data/temp3.json
src/schedules/data/temp4.json
src/schedules/FetchRooms.ts
src/schedules/Map.tsx
src/schedules/MapContext.tsx
src/schedules/Schedule.tsx
src/schedules/ScheduleMapPage.tsx
src/schedules/styles/BuildingPopup.css
src/schedules/styles/DaySelector.css
src/schedules/styles/Map.css
src/schedules/styles/Schedule.css
src/schedules/styles/ScheduleTimeBlock.css
src/types.d.ts
src/vite-env.d.ts
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vite.config.ts
```

### Dependencies

- backend/pyproject.toml: beautifulsoup4@>=4.14.3, fastapi[standard]@>=0.129.2, feedparser@>=6.0.12, httpx@>=0.28.1, idna@>=3.15, lxml@>=6.1.0, pygments@>=2.20.0, python-dotenv@>=1.2.2, python-multipart@>=0.0.27, requests@>=2.33.0, tqdm@>=4.67.3, urllib3@>=2.7.0, uvicorn@>=0.41.0
- backend/requirements.txt: beautifulsoup4, fastapi[standard], feedparser, httpx, lxml, requests, uvicorn
- package.json: @eslint/js@^9.39.4, @mantine/core@^7.17.8, @mantine/hooks@^7.17.8, @types/geojson@^7946.0.16, @types/leaflet@^1.9.21, @types/node@^25.9.2, @types/react@^19.2.15, @types/react-dom@^19.2.3, @vitejs/plugin-react-swc@^3.11.0, eslint@^9.39.4, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.26, globals@^15.15.0, leaflet@^1.9.4, postcss@^8.5.15, postcss-preset-mantine@^1.18.0, postcss-simple-vars@^7.0.1, react@19.0.0-rc.1, react-dom@19.0.0-rc.1, react-helmet-async@^3.0.0, react-leaflet@5.0.0-rc.2, react-router@^7.16.0, react-router-dom@^7.16.0, swiper@^12.2.0, typescript@~5.7.3, typescript-eslint@^8.60.0, vite@^6.4.2

### Recent commits (newest first)

- apply a fix to helmet
- add search schema generation to key pages
- add side margins in dateselector
- reduce yellowness of homepage buttons
- make homepage button link color darker
- big brother becomes bigger
- satisfying the draconian linter
- bump python dependencies
- bump dependencies
- Merge pull request #14 from UCSC-Websites/dependabot/npm_and_yarn/vite-6.4.2
- Bump vite from 6.4.1 to 6.4.2
- add image
- a
- readme about location schema
- ignore news and menu data
- moved analytics to separate file
- sections now show their instructor
- separated stuff out into its own files
- downloader now saves raw api response as binary
- update parser again to account for new format

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

### package.json

```
{
	"name": "cruzhacks2025",
	"private": true,
	"version": "0.0.0",
	"type": "module",
	"scripts": {
		"dev": "vite",
		"build": "tsc -b && vite build",
		"lint": "eslint .",
		"preview": "vite preview"
	},
	"dependencies": {
		"@mantine/core": "^7.17.8",
		"@mantine/hooks": "^7.17.8",
		"@types/leaflet": "^1.9.21",
		"leaflet": "^1.9.4",
		"react": "19.0.0-rc.1",
		"react-dom": "19.0.0-rc.1",
		"react-helmet-async": "^3.0.0",
		"react-leaflet": "5.0.0-rc.2",
		"react-router": "^7.16.0",
		"react-router-dom": "^7.16.0",
		"swiper": "^12.2.0"
	},
	"devDependencies": {
		"@eslint/js": "^9.39.4",
		"@types/geojson": "^7946.0.16",
		"@types/node": "^25.9.2",
		"@types/react": "^19.2.15",
		"@types/react-dom": "^19.2.3",
		"@vitejs/plugin-react-swc": "^3.11.0",
		"eslint": "^9.39.4",
		"eslint-plugin-react-hooks": "^5.2.0",
		"eslint-plugin-react-refresh": "^0.4.26",
		"globals": "^15.15.0",
		"postcss": "^8.5.15",
		"postcss-preset-mantine": "^1.18.0",
		"postcss-simple-vars": "^7.0.1",
		"typescript": "~5.7.3",
		"typescript-eslint": "^8.60.0",
		"vite": "^6.4.2"
	},
	"packageManager": "pnpm@9.14.3+sha512.c0f53ee99477ed969b82b289ad011a5d16bf1623c957e7f29eabe8d0c00b574c29b8c7f54f6c67ee710c73f285c8154d07ce44b46fe2c0eeb476a90441bac371"
}

```

### backend/requirements.txt

```
uvicorn
beautifulsoup4
fastapi[standard]
requests
httpx
feedparser
lxml


```

### backend/pyproject.toml

```
[project]
name = "backend"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "beautifulsoup4>=4.14.3",
    "fastapi[standard]>=0.129.2",
    "feedparser>=6.0.12",
    "httpx>=0.28.1",
    "idna>=3.15",
    "lxml>=6.1.0",
    "pygments>=2.20.0",
    "python-dotenv>=1.2.2",
    "python-multipart>=0.0.27",
    "requests>=2.33.0",
    "tqdm>=4.67.3",
    "urllib3>=2.7.0",
    "uvicorn>=0.41.0",
]

```

### src/main.tsx

```typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### backend/server.py

```python
from fastapi import FastAPI, Request
from fastapi.responses import Response
from fastapi.middleware.cors import CORSMiddleware
import endpoints.courses as courses
import endpoints.menu as menu
import endpoints.news as news
import endpoints.bigbrother as bigbrother
from locations import locations
from contextlib import asynccontextmanager
from bs4 import BeautifulSoup
import httpx, requests, uvicorn

# this function runs on startup
@asynccontextmanager
async def lifespan(app: FastAPI):
	locations.startup()
	bigbrother.startup()
	# await news.UpdateFeed()
	yield
	await bigbrother.shutdown()

api = FastAPI(lifespan=lifespan)
api.add_middleware(
	CORSMiddleware,
	allow_origins=[
	"http://localhost:5173",
	"http://localhost:8080",
	"https://ucsc.app",
	],
	allow_credentials=True,
	allow_methods=["GET", "POST"],
	allow_headers=["*"],
)

@api.get("/test")
async def getPath():
	return {"hello": "world"}


@api.get("/terms")
async def getTerms():
	response = requests.get("https://pisa.ucsc.edu/class_search/index.php")
	responseData: str = response.text
	soup = BeautifulSoup(responseData, 'lxml')
	termSelect = soup.find("select", {"id": "term_dropdown"}) #type: ignore
	options = termSelect.find_all("option") #type: ignore

	terms: list[int] = []
	for option in options:
		terms.append(int(option.get("value"))) #type: ignore

	return terms


api.include_router(news.router)
api.include_router(locations.router)
api.include_router(menu.router)
api.include_router(courses.router)
api.include_router(bigbrother.router)

if __name__ == '__main__':
	uvicorn.run('server:api', host='0.0.0.0', port=8000, reload=True)

```

### src/App.tsx

```typescript
import './App.css'
import Dashboard from './dashboard/Dashboard';

import { BrowserRouter, Route, Routes } from "react-router";
import RssFeed from './news/News.tsx';
import MenuPage from './menu/MenuPage.tsx';
import { Context } from './Context.tsx';

import Courses from './courses/Courses.tsx';
import { useEffect, useState } from 'react';
import { HelmetProvider } from 'react-helmet-async';

import ScheduleMapPage from './schedules/ScheduleMapPage.tsx';

function App() {
	const [mobile, setMobile] = useState(false);
	const [drawerOpen, setDrawerOpen] = useState(false);
	const [desktopMenuHeight, setDesktopMenuHeight] = useState(0);
	const [selectedDateOffset, setSelectedDateOffset] = useState(0);
	const [theme, setTheme] = useState(() =>
		localStorage.getItem("theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
	);
	useEffect(() => {
		const onResize = () => setMobile(window.innerWidth < 600);
		onResize();
		window.addEventListener('resize', onResize);
		return () => window.removeEventListener('resize', onResize);
	}, [setMobile]);
	useEffect(() => {
		document.documentElement.setAttribute("data-theme", theme);
		localStorage.setItem("theme", theme);
	}, [theme]);


	const contextValues = {
		mobile: mobile,
		isDrawerOpen: drawerOpen,
		setDrawerOpen: setDrawerOpen,

		desktopMenuHeight: desktopMenuHeight,
		setDesktopMenuHeight: setDesktopMenuHeight,

		theme: theme,
		setTheme: setTheme,

		selectedDateOffset: selectedDateOffset,
		setSelectedDateOffset: setSelectedDateOffset,
	}

	return (
		<HelmetProvider>
			<Context.Provider value={contextValues}>
				<BrowserRouter>
					{/* <SubdomainRouter /> */}
					<Routes>
						<Route path='/' element={<Dashboard />} />
						<Route path='/news' element={<RssFeed />} />
						<Route path='/courses' element={<Courses />} />
						<Route path='/menu' element={<MenuPage />} />
						<Route path='/map' element={<ScheduleMapPage />} />
					</Routes>
				</BrowserRouter>
			</Context.Provider>
		</HelmetProvider>
	);
}

export default App;
```

### eslint.config.js

```javascript
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";

export default tseslint.config(
	{ ignores: ["dist"] },
	{
		extends: [js.configs.recommended, ...tseslint.configs.recommended],
		files: ["**/*.{ts,tsx}"],
		languageOptions: {
			ecmaVersion: 2020,
			globals: globals.browser,
		},
		plugins: {
			"react-hooks": reactHooks,
			"react-refresh": reactRefresh,
		},
		rules: {
			...reactHooks.configs.recommended.rules,
			"react-refresh/only-export-components": [
				"warn",
				{ allowConstantExport: true },
			],
		},
	},
);

```

### vite.config.ts

```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import fs from "node:fs";
import path from "node:path";
import { ROUTE_LIST } from "./src/common/seo/routeMeta";

function escapeHtml(value: string) {
	return value
		.replace(/&/g, "&amp;")
		.replace(/"/g, "&quot;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;");
}

function prerenderMetaPlugin() {
	return {
		name: "prerender-meta",
		closeBundle() {
			const distDir = path.resolve("dist");
			const template = fs.readFileSync(
				path.join(distDir, "index.html"),
				"utf-8",
			);

			for (const route of ROUTE_LIST) {
				const url = route.url;
				const title = escapeHtml(route.title);
				const description = escapeHtml(route.description);
				const keywords = escapeHtml(route.keywords);

				// JSON-LD escaped so a stray "</script>" in data can't break out of the tag.
				const jsonLd = route.schema
					? `<script type="application/ld+json">${JSON.stringify(
							route.schema,
					  ).replace(/</g, "\\u003c")}</script>`
					: "";

				const html = template
					.replace(/<title>.*?<\/title>/, `<title>${title}</title>`)
					.replace(
						/<meta name="title"[^>]*>/,
						`<meta name="title" content="${title}" />`,
					)
					.replace(
						/<meta name="description"[^>]*>/,
						`<meta name="description" content="${description}" />`,
					)
					.replace(
						/<meta name="keywords"[^>]*>/,
						`<meta name="keywords" content="${keywords}" />`,
					)
					.replace(
						/<link rel="canonical"[^>]*>/,
						`<link rel="canonical" href="${url}" />`,
					)
					.replace(
						/<meta property="og:url"[^>]*>/,
						`<meta property="og:url" content="${url}" />`,
					)
					.replace(
						/<meta property="og:title"[^>]*>/,
						`<meta property="og:title" content="${title}" />`,
					)
					.replace(
						/<meta property="og:description"[^>]*>/,
						`<meta property="og:description" content="${description}" />`,
					)
					.replace(
						/<meta property="twitter:url"[^>]*>/,
						`<meta property="twitter:url" content="${url}" />`,
					)
					.replace(
						/<meta property="twitter:title"[^>]*>/,
						`<meta property="twitter:title" content="${title}" />`,
					)
					.replace(
						/<meta property="twitter:description"[^>]*>/,
						`<meta property="twitter:description" content="${description}" />`,
					)
					.replace(/<\/head>/, `${jsonLd}</head>`);

				if (route.path === "/") {
					fs.writeFileSync(path.join(distDir, "index.html"), html);
				} else {
					const routeDir = path.join(distDir, route.path);
					fs.mkdirSync(routeDir, { recursive: true });
					fs.writeFileSync(path.join(routeDir, "index.html"), html);
				}
			}
		},
	};
}

export default defineConfig({
	plugins: [react(), prerenderMetaPlugin()],
});

```

### index.html

```html
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<link rel="icon" type="image/x-icon" href="/favicon.ico" />
		<link rel="apple-touch-icon" href="/favicon.ico" />
		<link rel="manifest" href="/manifest.json" />
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1, viewport-fit=cover"
		/>

		<script async src="/s.js"></script>
		<script>
			((window.plausible =
				window.plausible ||
				function () {
					(plausible.q = plausible.q || []).push(arguments);
				}),
				(plausible.init =
					plausible.init ||
					function (i) {
						plausible.o = i || {};
					}));
			plausible.init();
		</script>

		<link rel="preconnect" href="https://api.ucsc.app"/>
		<link rel="dns-prefetch" href="https://api.ucsc.app"/>
		<link rel="preconnect" href="https://my.ucsc.edu"/>
		<link rel="dns-prefetch" href="https://my.ucsc.edu"/>

		<title>UCSC.app - UC Santa Cruz All-in-One Student Platform</title>
		<meta name="title" content="UCSC.app - UC Santa Cruz Student Platform"/>
		<meta name="description" content="Your all-in-one student platform for UCSC. Discover courses, explore dining options, view campus schedules, and stay updated with campus news."/>
		<meta name="keywords" content="UCSC, courses, dining, schedule, campus news, UC Santa Cruz"/>
		<meta name="author" content="UCSC.app" />
		<meta name="robots" content="index, follow" />

		<link rel="canonical" href="https://ucsc.app" />

		<meta property="og:type" content="website" />
		<meta property="og:url" content="https://ucsc.app/" />
		<meta property="og:title" content="UCSC.app - UC Santa Cruz Student Platform"/>
		<meta property="og:description" content="Your all-in-one student platform for UCSC. Discover courses, explore dining options, view campus schedules, and stay updated with campus news."/>
		<meta property="og:image" content="https://ucsc.app/favicon.ico"/>
		<meta property="og:site_name" content="UCSC.app"/>
		<meta property="og:locale" content="en_US"/>

		<meta property="twitter:card" content="summary_large_image" />
		<meta property="twitter:url" content="https://ucsc.app/" />
		<meta property="twitter:title" content="UCSC.app - UC Santa Cruz Student Platform"/>
		<meta property="twitter:description" content="Your all-in-one student platform for UCSC. Discover courses, explore dining options, view campus schedules, and stay updated with campus news."/>
		<meta property="twitter:image" content="https://ucsc.app/favicon.ico" />

		<meta name="theme-color" content="#000000" />
		<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
	</head>
	<body>
		<div id="root"></div>
		<script type="module" src="/src/main.tsx"></script>
		<script
			async
			src="https://scripts.simpleanalyticscdn.com/latest.js"
		></script>
		<script type="text/javascript">
			(function (l) {
				if (l.search[1] === "/") {
					var decoded = l.search
						.slice(1)
						.split("&")
						.map(function (s) {
							return s.replace(/~and~/g, "&");
						})
						.join("?");
					window.history.replaceState(
						null,
						null,
						l.pathname.slice(0, -1) + decoded + l.hash,
					);
				}
			})(window.location);
		</script>
	</body>
</html>

```

### src/declarations.d.ts

```typescript
declare module '*.css';
```

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