Project Info
This project did not submit a demo video on Devpost.
Inspiration
As current college students, we felt that many students were not taking advantage of the discounts provided by their .edu email. The problem isn't the lack of discounts, its the lack of accessibility. Discounts are scattered across brand sites, campus pages, directories, and random GitHub lists, with no single place to look. Most of the deals you find are expired or not for your school. We wanted one clean feed that knows you're a student the moment you sign in with your .edu email, and shows only the deals that are relevant to you.
What it does
EduDeals is a searchable, filterable catalog of verified student discounts. Sign in with your .edu email to unlock a paginated grid of deals you can search by brand or description, filter by category, and sort by name, expiry, or highest % off. A "For your school" tab matches deals to your campus from your email domain, and a "Saved" tab keeps your favorites. Expired deals are hidden automatically, expiring-soon deals are flagged, and your filters live in the URL so a view is shareable. The catalog itself is kept fresh by a Python pipeline that scrapes and extracts new deals into the database on its own.
How we built it
The frontend is React 19 + TypeScript + Tailwind CSS v4, talking directly to Supabase for Postgres storage and email/password auth — no backend of our own. Discounts are populated by a standalone Python pipeline: httpx + BeautifulSoup fetch campus and city deal pages, Google Gemini extracts structured deals from messy HTML in one batched call, clean GitHub markdown lists are parsed deterministically with regex, and everything is upserted into Postgres via psycopg using ON CONFLICT (redemption_url) so re-runs update existing rows instead of duplicating.
Challenges we ran into
Messy, inconsistent school data. The school field comes in as display names ("UC San Diego"), bare domains ("chapman.edu"), and the keyword "all" — matching a user's email domain to the right deals needed a normalization heuristic rather than an exact lookup. Deduping without losing real deals. The same brand legitimately appears on multiple campuses; we key dedup on normalized brand + host so the same listing scraped twice on one site collapses, but the same brand across different campuses doesn't. LLM cost and reliability. Sending every page in full to Gemini was wasteful, so we route only messy HTML to the LLM and parse clean, structured markdown deterministically instead.
Accomplishments we're proud of
A self-updating catalog built with the scraper means the deal list isn't hand-maintained, which is paired with personalized per-school feeds, shareable URL state, persistent dark mode, and a uniform card grid that adapts cleanly no matter how long a deal's description is. And a data pipeline that mixes LLM extraction with deterministic parsing to keep both speed and accuracy.
What we learned
How to combine an LLM with deterministic parsing so each approach does what it's best at, how to design dedup/upsert logic that's idempotent across re-runs, and how much of building a real product is taming inconsistent data rather than writing features. On the frontend, we got a lot more comfortable with React state that needs to stay in sync with the URL and localStorage at the same time.
What's next
A proper domain → school mapping (or a user-set school) so display-name campuses match correctly. Scheduling the scraper (cron/serverless) so the catalog refreshes automatically. One-click copy for promo codes, deal ratings/verification by students, and expiry notifications. Broadening sources beyond the initial California campuses.
EduDeals
A student-discount finder web application built with React, TypeScript, and Supabase. EduDeals helps students browse, search, and save verified discounts — with a personalized feed that surfaces deals for your school based on your .edu email. Discounts are populated automatically by a Python scraping pipeline.
Overview
Students sign in with a .edu email and land on a searchable grid of discounts. From there, they can:
- Browse all deals — paginated grid of discount cards, 9 per page
- Search & filter — match by brand/description, filter by one or more categories, and sort by name, expiry, or highest % off
- View your university-specific deals — a dedicated tab matches discounts against the user's email domain (e.g.
@g.ucla.edu→ schools containing "ucla") - Save favorites — heart any deal to add it to a personal "Saved" tab, persisted per user
- Read full details — long descriptions are clamped on the card, with a "Read more" link that opens a detail modal when text actually overflows
- Toggle dark mode — toggle persisted to
localStorage, defaulting to OS preference, with a glow-on-hover card effect
Filtered/sorted views are written to the URL, so a search can be shared or survives a page reload. The last-viewed tab is also remembered across sessions.
Behind the scenes, a standalone Python pipeline discovers deals from campus pages and seed lists, extracts them with Gemini, and upserts them into the same Postgres database the frontend reads from.
Tech Stack
- Frontend: React 19, TypeScript, Tailwind CSS v4 (
@tailwindcss/vite) - Backend: Supabase (Postgres database + email/password auth) — the React app talks to Supabase directly
- Data pipeline: Python,
httpx+BeautifulSoupfor fetching/parsing, Google Gemini for deal extraction,psycopgfor writing straight to Postgres - Build tooling: Vite, ESLint
The scraper writes to the same database out of band — the frontend never runs it directly.
Project Structure
client/
└── src/
├── components/ # DiscountGrid, DiscountCard, AuthBar, ThemeToggle, Toast, ResetPassword
├── library/ # Supabase client + useSession / useSavedDiscounts hooks
├── scraper/ # Python pipeline (main.py, config.py, database.py, pipeline_*.py, tests)
├── types.ts # the Discount type
└── App.tsx # app shell
How to Run
Prerequisites
Frontend
1. Clone the repository
git clone https://github.com/yourusername/edudeals.git
2. Navigate to the client directory
cd edudeals/client
3. Install dependencies
npm install
4. Configure environment variables
Create a .env (or .env.local) in client/ with your Supabase project credentials:
VITE_SUPABASE_URL=https://<your-project>.supabase.co
VITE_SUPABASE_ANON_KEY=<your-anon-key>
5. Set up the database
Supabase needs two tables:
| Table | Columns |
|---|---|
discounts | id, brand, description, discount_percent (text, e.g. "50% off"), category, redemption_url (unique — the scraper upserts on it), expires_at (nullable), created_at, school (nullable — a school name, an email domain like chapman.edu, or the literal all), tags (text array) |
saved_discounts | user_id, discount_id (a user's hearted discounts) |
Enable Email/Password auth in Supabase for sign-in, favoriting, and the school feed.
6. Start the dev server
npm run dev
Runs at
http://localhost:5173.
Other Commands
npm run build # type-check + production build to dist/
npm run preview # serve the production build locally
npm run lint # run ESLint
Data Pipeline (Python Scraper)
src/scraper/ is a standalone pipeline that discovers student-discount listings, extracts structured deals, and upserts them into the discounts table. The frontend never runs it — it's a separate batch job, run on demand or on a schedule.
main.py orchestrates four stages:
- Collect pages — fetches a curated list of campus/city deal pages (
LOCAL_URLSinconfig.py), raw GitHub markdown deal lists (GITHUB_SEED_URLS), and search-discovered directory pages.httpxhandles fetching, with ScraperAPI as a 403 fallback. - Extract deals — messy campus HTML is cleaned and sent in a single batched call to Google Gemini, which returns structured deals; clean GitHub markdown lists are parsed deterministically with regex (no LLM quota used).
- Dedupe & merge — collapses the same merchant repeated on one site (by normalized brand + host) while keeping genuinely distinct deals; GitHub wins on cross-source brand collisions.
- Upsert — writes to
public.discountsvia apsycopgasync connection pool, usingON CONFLICT (redemption_url)so re-runs update existing rows instead of duplicating. Ascraped_output_debug.jsondump is written alongside for inspection.
Running the Scraper
1. Navigate to the scraper directory
cd src/scraper
2. Install dependencies
pip install -r requirements.txt
3. Run the pipeline
python main.py
Reads
client/.env(two levels up).
Required and optional environment variables:
DATABASE_URL=postgresql://... # required — Supabase → Project Settings → Database → Connection string (URI), NOT the REST URL
GEMINI_API_KEY=... # required for LLM extraction of campus HTML
SERPAPI_KEY=... # optional — brand/directory discovery
SCRAPERAPI_KEY=... # optional — fallback for pages that return 403
To add a source, add its URL to LOCAL_URLS or GITHUB_SEED_URLS in config.py — the semantic extractor generalizes across layouts, so no per-site CSS selectors are needed. The test_*.py files cover DB connectivity, inserts, and discovery in isolation.
Deployment (Vercel)
The app lives in the client/ directory, not the repo root. In your Vercel project settings, set Root Directory to client and add the two VITE_SUPABASE_* environment variables. Vite is auto-detected — no further build configuration needed.
License
MIT
Analysis
View
Metric
- 11
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- CSSIn code
- HTMLIn code
- JavaScriptIn code
- PythonIn code
- ReactIn code
- SupabaseIn code
- Tailwind CSSIn code
- TypeScriptIn code
- VercelClaimed
8 of 9 appear in the indexed code. 1 claimed on Devpost could not be matched to code, which may simply mean the tool leaves no trace in the repository.
AI coding agents
No AI coding agent signals were found in this repository.
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
96 KB
Source files
26
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
oh-a-cai/edudeals
38 files · 821 KB · @ 3aef68a
Structure
Interface
8 files · 21%Screens, components and styles rendered to the user.
Application logic
18 files · 47%Domain rules, services and shared utilities.
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- Python52%
- TypeScript40%
- Markdown7%
- JavaScript1%
- HTML0%
- CSS0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
client/package.json
npm · 17- @supabase/supabase-js
- react
- react-dom
- +14 more
client/src/scraper/requirements.txt
pypi · 6- beautifulsoup4
- google-genai
- httpx
- psycopg[binary,pool]
- pydantic
- python-dotenv
package.json
npm · 2- @tailwindcss/vite
- tailwindcss
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
This project’s features have not been analysed yet.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.