Course search and schedule planner for UMD. Built it because picking GenEds every semester was annoying and I wanted something that could actually filter by requirement combinations.
Live: genterp.vercel.app
- Boolean GenEd search — type something like
DSNS OR (DVUP AND DSHS)and it actually parses it correctly - Department / level filtering — filter by dept code (
CMSC), level pattern (CMSC4XX), or exact course (CMSC131) - AI summaries — pros/cons and a vibe check per course, generated from PlanetTerp student reviews using Gemini
- Live seat counts — fetches real-time availability from umd.io when you expand a card, caches it back to Supabase
- Schedule builder — weekly calendar with conflict detection; star a section to add it, click the grid to add manual blocks
- Course tracker — pin sections across semesters, works without an account (everything is in localStorage)
- Dark mode — persisted, no flash on load
| Framework | Next.js 16 (App Router) |
| Database | Supabase (PostgreSQL) |
| Styling | Tailwind CSS 4 + shadcn/ui |
| State | Zustand + localStorage |
| Language | TypeScript strict mode |
| AI | Google Gemini 2.5 Flash Lite |
| Deployment | Vercel + cron jobs |
| Package manager | pnpm |
GenEd tags from umd.io come as a nested string[][] (array of requirement groups). I store two versions:
gen_ed_raw JSONB— the original nested structure, used for AND co-occurrence checksgen_ed_tags TEXT[]— flattened and GIN-indexed for fast overlap queries
The search bar has a small recursive-descent parser that converts boolean expressions into parameterized SQL. A JS post-filter on the results enforces the AND semantics precisely (both tags have to be in the same group, not just anywhere in the course).
Meeting times are stored as military integers (1045 = 10:45am) so conflict detection is just an integer range comparison.
- Vercel cron jobs sync courses at 6am and sections at 7am daily
- When you expand a card, it fetches fresh seat counts from umd.io and writes them back to Supabase in the background
- Falls back to cached DB data if umd.io is down
genterp/
├── app/
│ ├── api/
│ │ ├── search/ # main search endpoint
│ │ ├── course/[id]/ # course detail + live sections
│ │ └── sync/ # cron sync endpoints
│ ├── course/[id]/ # course detail page
│ └── page.tsx # search + calendar layout
├── components/
│ ├── search/ # SearchBar, DeptFilter, CourseCard, etc.
│ ├── calendar/ # WeekView, PinnedList
│ └── icons/ # custom SVG components
├── lib/
│ ├── search/parse-boolean.ts # the boolean parser
│ ├── conflict-detection.ts
│ └── semester.ts
├── store/schedule.ts # Zustand store (pins, stars, blocks)
└── scripts/ # admin scripts for AI summary generation
- umd.io — course catalog and sections (no auth)
- PlanetTerp — GPA and student reviews (no auth)
- Google Gemini — AI summary generation (admin script only)
pnpm install
cp .env.example .env.local
# fill in: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY,
# SUPABASE_SERVICE_ROLE_KEY, GOOGLE_API_KEY, CRON_SECRET
pnpm devTo regenerate AI summaries for a department:
pnpm tsx scripts/generate-ai-summaries.ts --dept CMSC --skip-existing
# dry run first if you want
pnpm tsx scripts/generate-ai-summaries.ts --dry-run --limit 5Vercel, with two cron jobs in vercel.json:
{
"crons": [
{ "path": "/api/sync/courses", "schedule": "0 6 * * *" },
{ "path": "/api/sync/sections", "schedule": "0 7 * * *" }
]
}Both require a CRON_SECRET Bearer token.