Build an AI Hackathon Discovery App
Build a full-stack app that uses an LLM to score and recommend hackathons.
Introduction
30 Second Summary
Hundreds of hackathons happen every month. Finding the right one to join based on your skills and interests can feel like searching for a needle in a haystack.
In this project, you will build a full-stack AI-powered web app that analyzes hackathon listings and recommends the best one for you. The app scores each event across multiple dimensions, then generates a project idea, tech stack, and pitch summary for your top pick.
What You'll Build
A SaaS-style Hackathon Discovery Assistant with a React frontend, FastAPI backend, and an LLM-powered scoring engine that evaluates and ranks hackathons based on your selected interests.
By the end of this project, you'll have:
- A React landing page with a hero section, feature highlights, and a call-to-action button that navigates to your dashboard.
- A FastAPI backend serving REST API endpoints for hackathon data and AI-powered analysis.
- An AI scoring engine that rates each hackathon on prize potential, difficulty, theme match, and team fit, plus generates a project idea, tech stack, and pitch summary for your top pick.
- Secret Mission: A side-by-side hackathon comparison feature with AI-generated pros and cons for each option.
Are there any prerequisites?
You'll need Python 3.10+, Node.js 20.19+, and VS Code installed on your machine. You also need a free Ollama account with an API key, which you can create at ollama.com/settings/keys.
Set Up the Project Structure
This project has two main pieces. A FastAPI backend that talks to an LLM, and a React frontend that gives users a polished interface to interact with it.
Before we write any application code, we need to scaffold both halves of the project and install the right dependencies. In this step, you'll set up a Python virtual environment for the backend and a Vite-powered React project for the frontend.
In this step, get ready to:
- Create the project folder and set up the FastAPI backend with a Python virtual environment.
- Scaffold the React frontend with Vite and install frontend dependencies.
- Configure Tailwind CSS and the development proxy.
Create the project folder and set up the backend
- Move to your Desktop and create the project structure by running these commands:
cd ~/Desktop
mkdir hackathon-discovery
cd hackathon-discovery
mkdir backend
cd backend
What do these commands do?
- cd ~/Desktop moves your terminal to the Desktop so the project folder is easy to find.
- mkdir hackathon-discovery creates the project root folder.
- mkdir backend creates the backend directory inside the project, and cd backend moves you into it.
- Confirm the folder was created by running:
ls ..
You should see backend in the output.
A virtual environment keeps this project's Python packages separate from any others on your machine. This prevents version conflicts between projects.
- Create and activate a Python virtual environment by running these commands:
macOS
python3 -m venv venv
source venv/bin/activate
Windows
python -m venv venv
venv\Scripts\activate
- Check your terminal prompt. You should see (venv) at the beginning of the line, confirming the virtual environment is active.
Don't see (venv) in your prompt?
- Make sure you ran the activate command, not just the python -m venv venv command. The first command creates the environment. The second activates it.
- On Windows, if you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell first, then try activating again.
Help me activate my Python virtual environment
- Install the backend dependencies by running:
pip install "fastapi[standard]" ollama
What gets installed here?
- fastapi[standard] installs FastAPI along with uvicorn (the dev server) and other standard dependencies like Pydantic for data validation.
- ollama is the Python client library for the Ollama API. You'll use it to send prompts to a cloud-hosted LLM.
- Verify FastAPI installed correctly by running:
python -c "import fastapi; print(fastapi.__version__)"
You should see a version number like 0.136.1 printed in the terminal.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is still active. You should see (venv) in your terminal prompt.
- If you closed and reopened your terminal, you need to re-activate the virtual environment before running the check.
Help me fix my FastAPI installation
Scaffold the React frontend with Vite
The backend is ready. Now you'll scaffold the frontend using Vite, a fast build tool for modern web projects.
- Navigate back to the project root by running:
cd ..
- Scaffold a new React project by running:
npm create vite@latest frontend -- --template react
What does this command do?
npm create vite@latest runs the Vite project scaffolder. frontend is the folder name it creates. --template react tells Vite to use the React starter template so you skip the interactive prompt.
If npm asks to install create-vite, type y and press Enter to continue.
You should see a message like Scaffolding project in ./frontend followed by instructions to install dependencies.
- Move into the frontend folder and install the base dependencies by running:
cd frontend
npm install
- Install Tailwind CSS, the Tailwind Vite plugin, and React Router by running:
npm install tailwindcss @tailwindcss/vite react-router-dom
Why these three packages?
- tailwindcss gives you utility-first CSS classes so you can style components without writing custom CSS files.
- @tailwindcss/vite is the Tailwind v4 plugin that integrates directly with Vite. No separate config file needed.
- react-router-dom lets you create multiple pages (landing page and dashboard) in a single-page React app.
You should see added X packages in the terminal output with no errors.
Seeing npm errors during install?
- Make sure you are inside the frontend folder, not the project root. Run pwd (macOS) or cd (Windows) to check your current directory.
- Confirm your Node.js version is 20.19 or higher by running node --version. Vite 8 requires at least 20.19.
Help me fix my npm install errors
Configure Tailwind CSS and the Vite proxy
The frontend has its packages installed, but Vite does not know about Tailwind or the backend yet. You need to register the Tailwind plugin and set up a proxy so the frontend can talk to FastAPI during development.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the hackathon-discovery folder, then click Open.
- In the Explorer sidebar, expand the frontend folder and open vite.config.js.
- Select all the existing code and replace it with the following:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
},
},
},
});
What does this config do?
- plugins: [react(), tailwindcss()] registers the React and Tailwind CSS plugins so Vite processes both JSX and utility classes.
- The proxy block forwards any request starting with /api to your FastAPI server at http://localhost:8000. This means the frontend can call /api/hackathons without worrying about CORS issues during development.
- Save vite.config.js.
Tailwind CSS v4 simplified its setup. All you need in the CSS file is a single import line.
- In the Explorer sidebar, expand frontend/src and open index.css.
- Select all the existing code and replace it with:
@import "tailwindcss";
- Save index.css.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the full vite.config.js:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
},
},
},
});
And the full index.css:
@import "tailwindcss";
Time to make sure everything is working. You'll start the Vite dev server to confirm the frontend compiles without errors.
- In your terminal, make sure you are still in the frontend folder.
- Start the Vite dev server by running:
npm run dev
You should see output that includes a local URL, typically http://localhost:5173/.
- Open http://localhost:5173 in your browser. You should see the default Vite + React starter page with a counter button.
- Go back to your terminal and press Ctrl+C to stop the dev server.
Dev server not starting?
- If you see an error about missing modules, make sure you ran npm install inside the frontend folder before starting the server.
- If the browser page is blank, check the terminal for error messages. A common cause is a typo in vite.config.js. Compare your file with the double-check tab above.
- If port 5173 is already in use, Vite will automatically pick another port and display it in the terminal output.
Help me get my Vite dev server running
Your development environment is ready with both servers configured and all dependencies installed. Next, you'll build the FastAPI backend with hackathon data and AI-powered analysis endpoints.
Build the FastAPI Backend with Hackathon Data
Your project folders are ready. Dependencies are installed, and both the frontend and backend toolchains are configured.
Now it's time to build the brain of your app. In this step, you'll create a FastAPI backend that stores hackathon data, exposes it through REST API endpoints, and uses an LLM to score and recommend hackathons based on a user's interests.
In this step, get ready to:
- Create a FastAPI app with CORS middleware and a simulated hackathon dataset.
- Build API endpoints that list hackathons, analyze them with AI scoring, and compare two side-by-side.
- Test the backend using FastAPI's built-in Swagger UI.
Set up the FastAPI app and hackathon data
The backend needs a few foundational pieces before it can serve any data. It needs an app instance, permission for the React frontend to call it, and a connection to the Ollama Cloud API for AI analysis.
- Create a new file called main.py inside your backend/ folder.
- Add the following setup code to main.py to create the app, configure CORS, and connect to Ollama Cloud:
import os
import json
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from ollama import Client
# Create the FastAPI app instance
app = FastAPI(title="Hackathon Discovery API")
# Allow the React frontend at localhost:5173 to call this API
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
# Connect to Ollama Cloud using your API key from the environment
ollama_client = Client(
host="https://ollama.com",
headers={"Authorization": "Bearer " + os.environ.get("OLLAMA_API_KEY", "")}
)
# The LLM model to use for hackathon analysis
MODEL_NAME = "gemma2:9b"
# Simulated hackathon dataset
HACKATHONS = [
{
"id": 1,
"name": "AI for Good Global Hackathon",
"theme": "AI/ML",
"description": "Build AI solutions that address social challenges. Focus on healthcare accessibility, education equity, and environmental monitoring.",
"prize_pool": "$50,000",
"team_size_range": "2-5",
"difficulty": "Intermediate",
"deadline": "2026-06-15",
"organizer": "TechForChange Foundation"
},
{
"id": 2,
"name": "DeFi Builder Sprint",
"theme": "Web3",
"description": "Create decentralized finance protocols and tools. Emphasis on cross-chain interoperability, yield optimization, and user-friendly wallet experiences.",
"prize_pool": "$75,000",
"team_size_range": "3-6",
"difficulty": "Advanced",
"deadline": "2026-05-30",
"organizer": "ChainLabs"
},
{
"id": 3,
"name": "HealthTech Innovation Challenge",
"theme": "Healthcare",
"description": "Develop digital health solutions for patient care. Areas include telemedicine, mental health apps, wearable data analytics, and clinical workflow automation.",
"prize_pool": "$30,000",
"team_size_range": "2-4",
"difficulty": "Intermediate",
"deadline": "2026-07-01",
"organizer": "MedInnovate Labs"
},
{
"id": 4,
"name": "FinTech Disrupt 2026",
"theme": "Fintech",
"description": "Reimagine financial services with technology. Focus areas are embedded finance, neobanking, fraud detection, and AI-powered credit scoring.",
"prize_pool": "$100,000",
"team_size_range": "3-5",
"difficulty": "Advanced",
"deadline": "2026-06-20",
"organizer": "Global FinTech Forum"
},
{
"id": 5,
"name": "SaaS Startup Weekend",
"theme": "SaaS",
"description": "Build a micro-SaaS product in 48 hours. Emphasis on finding a niche, rapid prototyping, and demonstrating product-market fit with real users.",
"prize_pool": "$15,000",
"team_size_range": "1-3",
"difficulty": "Beginner",
"deadline": "2026-05-25",
"organizer": "IndieHackers Collective"
},
{
"id": 6,
"name": "GameJam Universe",
"theme": "Gaming",
"description": "Create an original game from scratch. Themes rotate each round. Judging criteria include gameplay innovation, art direction, and technical execution.",
"prize_pool": "$20,000",
"team_size_range": "1-4",
"difficulty": "Intermediate",
"deadline": "2026-06-10",
"organizer": "PixelForge Studios"
},
{
"id": 7,
"name": "Climate Data Challenge",
"theme": "Climate",
"description": "Use open climate datasets to build tools for sustainability. Projects can address carbon tracking, renewable energy optimization, or biodiversity monitoring.",
"prize_pool": "$40,000",
"team_size_range": "2-5",
"difficulty": "Intermediate",
"deadline": "2026-07-15",
"organizer": "GreenTech Alliance"
},
{
"id": 8,
"name": "EduTech Builders Jam",
"theme": "Education",
"description": "Build tools that improve learning outcomes. Focus on adaptive learning, AI tutoring, gamification of education, and accessibility for underserved communities.",
"prize_pool": "$25,000",
"team_size_range": "2-4",
"difficulty": "Beginner",
"deadline": "2026-06-05",
"organizer": "LearnForward Initiative"
}
]
What does this code do?
This sets up the four pillars of your backend:
- FastAPI(title=...) creates the app instance that all your endpoints attach to.
- The CORS middleware tells the browser it's OK for your React frontend (running on port 5173) to make requests to this API (running on port 8000). Without it, the browser blocks cross-origin requests.
- The Client connects to Ollama Cloud using an API key stored in the OLLAMA_API_KEY environment variable. The gemma2:9b model is available on the free tier and handles structured JSON output well.
- HACKATHONS is a list of eight simulated hackathon listings. Each entry has nine fields (like theme, prize_pool, difficulty) that the LLM will use to make scoring decisions.
- Save main.py.
You won't see any output yet. The next chunk adds the endpoints that serve and analyze this data.
Seeing a red underline on an import?
Make sure your virtual environment is activated. Your terminal prompt should show (venv) at the start. If it doesn't, navigate to the backend/ folder and activate it again.
Help me fix import errors in my FastAPI project
Create the API endpoints
Your backend needs three endpoints. One returns the raw hackathon list. One sends hackathon data to the LLM for scoring. One compares two specific hackathons head-to-head.
First, you need Pydantic models to define the shape of incoming request bodies. Then you'll build the listing endpoint and the AI-powered analysis endpoint.
- In main.py, add the following code below the HACKATHONS list:
# Define the expected shape of incoming request bodies
class AnalyzeRequest(BaseModel):
interests: list[str]
class CompareRequest(BaseModel):
interests: list[str]
hackathon_1: str
hackathon_2: str
# Return all hackathon listings
@app.get("/api/hackathons")
def get_hackathons():
return {"hackathons": HACKATHONS}
@app.post("/api/analyze")
def analyze_hackathons(request: AnalyzeRequest):
if not request.interests:
raise HTTPException(status_code=400, detail="At least one interest is required")
# Build a text summary of every hackathon for the prompt
hackathon_summaries = ""
for h in HACKATHONS:
hackathon_summaries += (
f"- {h['name']} | Theme: {h['theme']} | Prize: {h['prize_pool']} "
f"| Team Size: {h['team_size_range']} | Difficulty: {h['difficulty']} "
f"| Deadline: {h['deadline']}\n Description: {h['description']}\n\n"
)
# Construct a detailed prompt that asks the LLM to score and recommend
prompt = f"""You are a hackathon advisor. A user is interested in: {', '.join(request.interests)}.
Here are the available hackathons:
{hackathon_summaries}
For each hackathon, provide scores from 1-10 for:
- prize_potential: How attractive is the prize relative to effort?
- difficulty: How challenging is this hackathon? (1=very hard, 10=very easy for this user)
- theme_match: How well does this hackathon match the user's interests?
- team_fit: How accessible is the team size requirement?
Then pick the BEST hackathon for this user and generate:
- A project idea (2-3 sentences)
- A recommended tech stack (list of technologies)
- A short pitch summary (2-3 sentences)
Respond ONLY with valid JSON in this exact format, no other text:
{{
"scores": [
{{
"hackathon_name": "name here",
"prize_potential": 8,
"difficulty": 7,
"theme_match": 9,
"team_fit": 8,
"overall": 8.0
}}
],
"top_pick": {{
"hackathon_name": "name here",
"project_idea": "Your idea here",
"tech_stack": ["Tech1", "Tech2", "Tech3"],
"pitch_summary": "Your pitch here"
}}
}}"""
# Send the prompt to Ollama Cloud and parse the JSON response
try:
response = ollama_client.chat(
model=MODEL_NAME,
messages=[{"role": "user", "content": prompt}],
format="json"
)
result = json.loads(response.message.content)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"AI analysis failed: {str(e)}")
What does this code do?
- AnalyzeRequest and CompareRequest tell FastAPI what shape of JSON body each endpoint expects. FastAPI validates incoming requests against these models automatically.
- GET /api/hackathons returns the entire hackathon list as JSON. The frontend will call this to display listings.
- POST /api/analyze is the core AI feature. It loops through every hackathon to build a text summary, sends a detailed prompt to the LLM, and asks it to score each hackathon on four dimensions: prize potential, difficulty, theme match, and team fit.
- format="json" tells Ollama to constrain the model's output to valid JSON. This makes parsing the response reliable.
Why include an example JSON structure in the prompt?
LLMs follow instructions better when you show them the exact output format you expect. The double curly braces {{ and }} are Python f-string escapes that produce literal { and } in the final prompt text.
- Save main.py.
You won't see a change yet. One more endpoint to add.
The final endpoint lets users compare two specific hackathons head-to-head. It looks up both hackathons by name, sends them to the LLM, and gets back pros, cons, and a recommendation.
- In main.py, add the compare endpoint below the analyze_hackathons function:
@app.post("/api/compare")
def compare_hackathons(request: CompareRequest):
# Look up both hackathons by name
h1 = next((h for h in HACKATHONS if h["name"] == request.hackathon_1), None)
h2 = next((h for h in HACKATHONS if h["name"] == request.hackathon_2), None)
if not h1 or not h2:
raise HTTPException(status_code=404, detail="One or both hackathons not found")
# Build a comparison prompt with both hackathons' details
prompt = f"""You are a hackathon advisor. A user interested in {', '.join(request.interests)} wants to compare two hackathons.
Hackathon 1: {h1['name']}
Theme: {h1['theme']} | Prize: {h1['prize_pool']} | Team Size: {h1['team_size_range']} | Difficulty: {h1['difficulty']} | Deadline: {h1['deadline']}
Description: {h1['description']}
Hackathon 2: {h2['name']}
Theme: {h2['theme']} | Prize: {h2['prize_pool']} | Team Size: {h2['team_size_range']} | Difficulty: {h2['difficulty']} | Deadline: {h2['deadline']}
Description: {h2['description']}
Compare these two hackathons for this user. Respond ONLY with valid JSON:
{{
"hackathon_1": {{
"name": "{h1['name']}",
"pros": ["pro1", "pro2", "pro3"],
"cons": ["con1", "con2"]
}},
"hackathon_2": {{
"name": "{h2['name']}",
"pros": ["pro1", "pro2", "pro3"],
"cons": ["con1", "con2"]
}},
"recommendation": "Which one the user should pick and a brief reason why"
}}"""
# Send to Ollama Cloud and parse the response
try:
response = ollama_client.chat(
model=MODEL_NAME,
messages=[{"role": "user", "content": prompt}],
format="json"
)
result = json.loads(response.message.content)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Comparison failed: {str(e)}")
What does this endpoint do?
- next((h for h in HACKATHONS if ...), None) searches the hackathon list for a match by name. If no match is found, it returns None.
- The prompt gives the LLM both hackathons' full details and asks for pros, cons, and a final recommendation.
- The same format="json" pattern ensures the response is parseable JSON.
- Save main.py.
Your backend file is complete. Time to double-check it before testing.
✔️ Awesome, I've got everything!
Make sure you've saved main.py before moving on.
ⓧ I'd like to double check the full code
import os
import json
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from ollama import Client
app = FastAPI(title="Hackathon Discovery API")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
ollama_client = Client(
host="https://ollama.com",
headers={"Authorization": "Bearer " + os.environ.get("OLLAMA_API_KEY", "")}
)
MODEL_NAME = "gemma2:9b"
HACKATHONS = [
{
"id": 1,
"name": "AI for Good Global Hackathon",
"theme": "AI/ML",
"description": "Build AI solutions that address social challenges. Focus on healthcare accessibility, education equity, and environmental monitoring.",
"prize_pool": "$50,000",
"team_size_range": "2-5",
"difficulty": "Intermediate",
"deadline": "2026-06-15",
"organizer": "TechForChange Foundation"
},
{
"id": 2,
"name": "DeFi Builder Sprint",
"theme": "Web3",
"description": "Create decentralized finance protocols and tools. Emphasis on cross-chain interoperability, yield optimization, and user-friendly wallet experiences.",
"prize_pool": "$75,000",
"team_size_range": "3-6",
"difficulty": "Advanced",
"deadline": "2026-05-30",
"organizer": "ChainLabs"
},
{
"id": 3,
"name": "HealthTech Innovation Challenge",
"theme": "Healthcare",
"description": "Develop digital health solutions for patient care. Areas include telemedicine, mental health apps, wearable data analytics, and clinical workflow automation.",
"prize_pool": "$30,000",
"team_size_range": "2-4",
"difficulty": "Intermediate",
"deadline": "2026-07-01",
"organizer": "MedInnovate Labs"
},
{
"id": 4,
"name": "FinTech Disrupt 2026",
"theme": "Fintech",
"description": "Reimagine financial services with technology. Focus areas are embedded finance, neobanking, fraud detection, and AI-powered credit scoring.",
"prize_pool": "$100,000",
"team_size_range": "3-5",
"difficulty": "Advanced",
"deadline": "2026-06-20",
"organizer": "Global FinTech Forum"
},
{
"id": 5,
"name": "SaaS Startup Weekend",
"theme": "SaaS",
"description": "Build a micro-SaaS product in 48 hours. Emphasis on finding a niche, rapid prototyping, and demonstrating product-market fit with real users.",
"prize_pool": "$15,000",
"team_size_range": "1-3",
"difficulty": "Beginner",
"deadline": "2026-05-25",
"organizer": "IndieHackers Collective"
},
{
"id": 6,
"name": "GameJam Universe",
"theme": "Gaming",
"description": "Create an original game from scratch. Themes rotate each round. Judging criteria include gameplay innovation, art direction, and technical execution.",
"prize_pool": "$20,000",
"team_size_range": "1-4",
"difficulty": "Intermediate",
"deadline": "2026-06-10",
"organizer": "PixelForge Studios"
},
{
"id": 7,
"name": "Climate Data Challenge",
"theme": "Climate",
"description": "Use open climate datasets to build tools for sustainability. Projects can address carbon tracking, renewable energy optimization, or biodiversity monitoring.",
"prize_pool": "$40,000",
"team_size_range": "2-5",
"difficulty": "Intermediate",
"deadline": "2026-07-15",
"organizer": "GreenTech Alliance"
},
{
"id": 8,
"name": "EduTech Builders Jam",
"theme": "Education",
"description": "Build tools that improve learning outcomes. Focus on adaptive learning, AI tutoring, gamification of education, and accessibility for underserved communities.",
"prize_pool": "$25,000",
"team_size_range": "2-4",
"difficulty": "Beginner",
"deadline": "2026-06-05",
"organizer": "LearnForward Initiative"
}
]
class AnalyzeRequest(BaseModel):
interests: list[str]
class CompareRequest(BaseModel):
interests: list[str]
hackathon_1: str
hackathon_2: str
@app.get("/api/hackathons")
def get_hackathons():
return {"hackathons": HACKATHONS}
@app.post("/api/analyze")
def analyze_hackathons(request: AnalyzeRequest):
if not request.interests:
raise HTTPException(status_code=400, detail="At least one interest is required")
hackathon_summaries = ""
for h in HACKATHONS:
hackathon_summaries += (
f"- {h['name']} | Theme: {h['theme']} | Prize: {h['prize_pool']} "
f"| Team Size: {h['team_size_range']} | Difficulty: {h['difficulty']} "
f"| Deadline: {h['deadline']}\n Description: {h['description']}\n\n"
)
prompt = f"""You are a hackathon advisor. A user is interested in: {', '.join(request.interests)}.
Here are the available hackathons:
{hackathon_summaries}
For each hackathon, provide scores from 1-10 for:
- prize_potential: How attractive is the prize relative to effort?
- difficulty: How challenging is this hackathon? (1=very hard, 10=very easy for this user)
- theme_match: How well does this hackathon match the user's interests?
- team_fit: How accessible is the team size requirement?
Then pick the BEST hackathon for this user and generate:
- A project idea (2-3 sentences)
- A recommended tech stack (list of technologies)
- A short pitch summary (2-3 sentences)
Respond ONLY with valid JSON in this exact format, no other text:
{{
"scores": [
{{
"hackathon_name": "name here",
"prize_potential": 8,
"difficulty": 7,
"theme_match": 9,
"team_fit": 8,
"overall": 8.0
}}
],
"top_pick": {{
"hackathon_name": "name here",
"project_idea": "Your idea here",
"tech_stack": ["Tech1", "Tech2", "Tech3"],
"pitch_summary": "Your pitch here"
}}
}}"""
try:
response = ollama_client.chat(
model=MODEL_NAME,
messages=[{"role": "user", "content": prompt}],
format="json"
)
result = json.loads(response.message.content)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"AI analysis failed: {str(e)}")
@app.post("/api/compare")
def compare_hackathons(request: CompareRequest):
h1 = next((h for h in HACKATHONS if h["name"] == request.hackathon_1), None)
h2 = next((h for h in HACKATHONS if h["name"] == request.hackathon_2), None)
if not h1 or not h2:
raise HTTPException(status_code=404, detail="One or both hackathons not found")
prompt = f"""You are a hackathon advisor. A user interested in {', '.join(request.interests)} wants to compare two hackathons.
Hackathon 1: {h1['name']}
Theme: {h1['theme']} | Prize: {h1['prize_pool']} | Team Size: {h1['team_size_range']} | Difficulty: {h1['difficulty']} | Deadline: {h1['deadline']}
Description: {h1['description']}
Hackathon 2: {h2['name']}
Theme: {h2['theme']} | Prize: {h2['prize_pool']} | Team Size: {h2['team_size_range']} | Difficulty: {h2['difficulty']} | Deadline: {h2['deadline']}
Description: {h2['description']}
Compare these two hackathons for this user. Respond ONLY with valid JSON:
{{
"hackathon_1": {{
"name": "{h1['name']}",
"pros": ["pro1", "pro2", "pro3"],
"cons": ["con1", "con2"]
}},
"hackathon_2": {{
"name": "{h2['name']}",
"pros": ["pro1", "pro2", "pro3"],
"cons": ["con1", "con2"]
}},
"recommendation": "Which one the user should pick and a brief reason why"
}}"""
try:
response = ollama_client.chat(
model=MODEL_NAME,
messages=[{"role": "user", "content": prompt}],
format="json"
)
result = json.loads(response.message.content)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Comparison failed: {str(e)}")
Test the backend with Swagger UI
FastAPI generates interactive API docs automatically. You can use them to test every endpoint without writing any frontend code.
Before starting the server, you need to set your Ollama API key as an environment variable so the backend can authenticate with Ollama Cloud.
- Make sure your terminal is in the backend/ folder with the virtual environment activated.
- Set your Ollama API key by running the command for your platform:
macOS
export OLLAMA_API_KEY=your-api-key-here
Windows
If you are using Command Prompt, run:
set OLLAMA_API_KEY=your-api-key-here
If you are using PowerShell, run:
$env:OLLAMA_API_KEY="your-api-key-here"
- Replace your-api-key-here with the API key from your Ollama account at ollama.com/settings/keys.
Where do I get an Ollama API key?
Sign in at ollama.com and go to Settings, then Keys. Click Create Key to generate a new API key. The free tier gives you access to models like gemma2:9b with light usage limits.
- Start the FastAPI development server by running:
uvicorn main:app --reload
What does this command do?
uvicorn is the server that runs your FastAPI app. main:app tells it to look for the app object in main.py. The --reload flag auto-restarts the server when you save changes.
- You should see output in the terminal that includes Uvicorn running on http://127.0.0.1:8000.
- Open your browser and go to http://localhost:8000/docs.
You should see the FastAPI Swagger UI with three endpoints listed: GET /api/hackathons, POST /api/analyze, and POST /api/compare.
- Click on GET /api/hackathons to expand it.
- Click Try it out.
- Click Execute.
You should see a JSON response with all eight hackathon listings.
- Now click on POST /api/analyze to expand it.
- Click Try it out.
- Replace the request body with this test payload:
{
"interests": ["AI/ML", "Healthcare"]
}
- Click Execute.
What should I see?
The response may take 10-30 seconds while the LLM processes your request. You should get back a JSON object with a scores array (each hackathon scored on four dimensions) and a top_pick object with a project idea, tech stack, and pitch summary.
Seeing a 500 error or "AI analysis failed"?
This usually means the Ollama API key is missing or invalid. Check that you set the OLLAMA_API_KEY environment variable in the same terminal session where you ran uvicorn. If you set the variable after starting the server, stop the server with Ctrl+C, set the variable, and start it again.
Also confirm your API key is valid at ollama.com/settings/keys. The free tier has light usage limits that reset weekly.
Help me debug my Ollama API connection in FastAPI
Server won't start?
Make sure you're in the backend/ folder (not the project root). Also check that your virtual environment is activated. You should see (venv) in your terminal prompt.
If you see a syntax error, double-check your main.py against the full code in the double-check tab above.
Help me fix my uvicorn startup error
Your backend is serving data and generating AI-powered recommendations. Next up, you'll build the landing page that welcomes users to your app.
Build the Landing Page
Your backend is ready to serve hackathon data and run AI-powered analysis. Now it's time to build the frontend that users actually interact with.
In this step, you'll set up React Router to handle page navigation and build a polished SaaS-style landing page. The landing page is the first thing users see, so it needs a clear hero section and feature highlights that explain what the app does.
In this step, get ready to:
- Set up React Router with routes for the landing page and dashboard.
- Build a landing page with a hero section, call-to-action button, and feature cards.
Set up React Router and page components
Your app needs two pages: a landing page at / and a dashboard at /dashboard. React Router maps URL paths to different components so users can navigate between them without a full page reload.
- Open a terminal in your frontend folder.
- Create the src/pages/ directory by running this command:
mkdir src/pages
Before you wire up the router, you need placeholder components for each page so the imports resolve without errors.
- Create a new file at src/pages/DashboardPage.jsx inside the frontend folder and add this placeholder code:
function DashboardPage() {
return <div>Dashboard coming soon</div>;
}
export default DashboardPage;
- Create another new file at src/pages/LandingPage.jsx and add this temporary placeholder:
function LandingPage() {
return <div>Landing page coming soon</div>;
}
export default LandingPage;
You'll replace this placeholder with the full landing page in the next substep.
Now you can set up the router. The current App.jsx file contains the default Vite boilerplate. You'll replace it entirely with your router configuration.
- Open src/App.jsx in VS Code and replace its entire contents with this code:
import { BrowserRouter, Routes, Route } from "react-router-dom";
import LandingPage from "./pages/LandingPage";
import DashboardPage from "./pages/DashboardPage";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
What does this code do?
BrowserRouter wraps your entire app and enables client-side routing. Routes acts as a container for all your route definitions.
Each Route maps a URL path to a component. When the user visits /, React renders LandingPage. When they visit /dashboard, React renders DashboardPage.
- Save App.jsx.
- Make sure your Vite dev server is running. If it's not, start it by running this command from the frontend directory:
npm run dev
- Open http://localhost:5173 in your browser. You should see the text "Landing page coming soon" on a blank white page.
- Navigate to http://localhost:5173/dashboard in your browser. You should see "Dashboard coming soon".
Seeing a blank page or an error?
- Check that DashboardPage.jsx and LandingPage.jsx are inside the src/pages/ directory. If you accidentally put them in src/ directly, the import paths in App.jsx won't resolve.
- Make sure each file has an export default statement at the bottom. Without it, the import in App.jsx can't find the component.
- Check your terminal for error messages. Vite shows detailed import resolution errors that point to the exact file.
Why isn't my React Router setup working?
✔️ Awesome, I've got everything!
Great. Make sure you've saved App.jsx before moving on.
ⓧ I'd like to double check the full code
import { BrowserRouter, Routes, Route } from "react-router-dom";
import LandingPage from "./pages/LandingPage";
import DashboardPage from "./pages/DashboardPage";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Build the landing page component
The landing page is the first impression of your app. You'll build it with a navigation bar, a hero section with a call-to-action, and a grid of feature cards that explain what the tool does.
Start by defining the data for the three feature cards. Keeping this data in a separate array outside the component makes it easy to update or extend later.
- Open src/pages/LandingPage.jsx and replace the entire placeholder with this import and features data:
import { useNavigate } from "react-router-dom";
// Data for the three feature highlight cards
const features = [
{
title: "AI-Powered Matching",
description:
"Our AI analyzes hackathon themes and matches them to your specific interests and skill level.",
icon: "\ud83c\udfaf",
},
{
title: "Smart Scoring",
description:
"Every hackathon is scored across prize potential, difficulty, theme match, and team fit.",
icon: "\ud83d\udcca",
},
{
title: "Pitch Generator",
description:
"Get a ready-to-go project idea, tech stack, and pitch summary for your top hackathon pick.",
icon: "\ud83d\ude80",
},
];
What does this code do?
- useNavigate is a React Router hook that lets you programmatically send users to a different page. You'll use it to navigate from the landing page to the dashboard when the user clicks a button.
- The features array holds the title, description, and icon for each feature card. Defining data outside the component avoids recreating it on every render.
Now add the component function that renders the nav bar, hero section, and features grid.
- In the same LandingPage.jsx file, add this component function below the features array:
function LandingPage() {
// Hook for navigating to the dashboard
const navigate = useNavigate();
return (
<div className="min-h-screen bg-gray-950 text-white">
{/* Navigation bar */}
<nav className="flex items-center justify-between px-8 py-5 max-w-6xl mx-auto">
<span className="text-xl font-bold text-indigo-400">HackMatch AI</span>
<button
onClick={() => navigate("/dashboard")}
className="text-sm text-gray-300 hover:text-white transition"
>
Dashboard →
</button>
</nav>
{/* Hero section with headline and call-to-action */}
<section className="text-center py-24 px-4 max-w-4xl mx-auto">
<h1 className="text-5xl font-extrabold mb-6 leading-tight">
Find Your Perfect{" "}
<span className="text-indigo-400">Hackathon</span>
</h1>
<p className="text-lg text-gray-400 mb-10 max-w-2xl mx-auto">
Tell us your interests and our AI will analyze upcoming hackathons,
score them for you, and generate a winning project pitch.
</p>
<button
onClick={() => navigate("/dashboard")}
className="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold px-8 py-3 rounded-lg text-lg transition"
>
Get Started Free
</button>
</section>
{/* Feature cards grid */}
<section className="max-w-6xl mx-auto px-8 pb-24">
<div className="grid md:grid-cols-3 gap-8">
{features.map((feature) => (
<div
key={feature.title}
className="bg-gray-900 border border-gray-800 rounded-xl p-6"
>
<div className="text-4xl mb-4">{feature.icon}</div>
<h3 className="text-lg font-semibold mb-2">{feature.title}</h3>
<p className="text-gray-400 text-sm">{feature.description}</p>
</div>
))}
</div>
</section>
</div>
);
}
export default LandingPage;
What does this code do?
- The nav bar shows the app name "HackMatch AI" on the left and a "Dashboard" link on the right. Both navigation actions use navigate("/dashboard") to switch pages without a full reload.
- The hero section contains the main headline, a subtitle explaining what the app does, and a prominent "Get Started Free" button.
- The features section uses features.map() to loop over the data array and render a styled card for each feature. The md:grid-cols-3 class makes the cards display in three columns on medium-sized screens and above.
- Save LandingPage.jsx.
- Open http://localhost:5173 in your browser (or refresh if it's already open). You should see a dark-themed landing page with "Find Your Perfect Hackathon" as the headline, a purple "Get Started Free" button, and three feature cards below.
- Click the Get Started Free button. You should be taken to the dashboard page showing "Dashboard coming soon".
Page looks unstyled or all white?
- Make sure your src/index.css file contains @import "tailwindcss"; and nothing else. If the Tailwind CSS import is missing, none of the utility classes will apply.
- Check that vite.config.js includes the tailwindcss() plugin in the plugins array. Without it, Vite doesn't process Tailwind classes.
- If the "Get Started Free" button doesn't navigate anywhere, make sure you imported useNavigate from react-router-dom at the top of the file.
Why isn't my Tailwind CSS styling showing up on the landing page?
✔️ Awesome, I've got everything!
Great. Double check you've saved LandingPage.jsx before moving on.
ⓧ I'd like to double check the full code
import { useNavigate } from "react-router-dom";
const features = [
{
title: "AI-Powered Matching",
description:
"Our AI analyzes hackathon themes and matches them to your specific interests and skill level.",
icon: "\ud83c\udfaf",
},
{
title: "Smart Scoring",
description:
"Every hackathon is scored across prize potential, difficulty, theme match, and team fit.",
icon: "\ud83d\udcca",
},
{
title: "Pitch Generator",
description:
"Get a ready-to-go project idea, tech stack, and pitch summary for your top hackathon pick.",
icon: "\ud83d\ude80",
},
];
function LandingPage() {
const navigate = useNavigate();
return (
<div className="min-h-screen bg-gray-950 text-white">
<nav className="flex items-center justify-between px-8 py-5 max-w-6xl mx-auto">
<span className="text-xl font-bold text-indigo-400">HackMatch AI</span>
<button
onClick={() => navigate("/dashboard")}
className="text-sm text-gray-300 hover:text-white transition"
>
Dashboard →
</button>
</nav>
<section className="text-center py-24 px-4 max-w-4xl mx-auto">
<h1 className="text-5xl font-extrabold mb-6 leading-tight">
Find Your Perfect{" "}
<span className="text-indigo-400">Hackathon</span>
</h1>
<p className="text-lg text-gray-400 mb-10 max-w-2xl mx-auto">
Tell us your interests and our AI will analyze upcoming hackathons,
score them for you, and generate a winning project pitch.
</p>
<button
onClick={() => navigate("/dashboard")}
className="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold px-8 py-3 rounded-lg text-lg transition"
>
Get Started Free
</button>
</section>
<section className="max-w-6xl mx-auto px-8 pb-24">
<div className="grid md:grid-cols-3 gap-8">
{features.map((feature) => (
<div
key={feature.title}
className="bg-gray-900 border border-gray-800 rounded-xl p-6"
>
<div className="text-4xl mb-4">{feature.icon}</div>
<h3 className="text-lg font-semibold mb-2">{feature.title}</h3>
<p className="text-gray-400 text-sm">{feature.description}</p>
</div>
))}
</div>
</section>
</div>
);
}
export default LandingPage;
Your landing page is live with a professional look and working navigation. Next up, you'll build the interactive dashboard where users select their interests and get AI-powered hackathon recommendations.
Build the Dashboard UI
Your landing page looks great and has a clear call-to-action directing users to the dashboard. The dashboard is where the real interaction happens.
Users need a way to tell the app what they care about, fire off an AI analysis, and see the results. In this step, you will build the entire dashboard UI with interest selection, API integration, score cards, and a highlighted top recommendation.
In this step, get ready to:
- Build an interactive interest selection panel with toggleable tags.
- Wire up the API integration and display hackathon score cards with progress bars.
- Create a highlighted top recommendation section with AI-generated project ideas.
Build the interest selection panel
The dashboard needs to collect the user's interests before it can run an AI analysis. You will build a panel of clickable tags that toggle on and off, plus the functions that power the entire dashboard flow.
- Open DashboardPage.jsx in the frontend/src/pages/ folder.
- Select all the placeholder code and replace it with the following:
import { useState } from "react";
import { useNavigate } from "react-router-dom";
// Interest categories users can select from
const INTEREST_OPTIONS = [
"AI/ML",
"Web3",
"Healthcare",
"Fintech",
"SaaS",
"Gaming",
"Climate",
"Education",
];
function DashboardPage() {
const navigate = useNavigate();
// Track which interest tags are selected
const [selectedInterests, setSelectedInterests] = useState([]);
// Store the AI analysis results
const [results, setResults] = useState(null);
// Track loading state for the API call
const [loading, setLoading] = useState(false);
// Store any error messages
const [error, setError] = useState(null);
// Add or remove an interest from the selection
function toggleInterest(interest) {
setSelectedInterests((prev) =>
prev.includes(interest)
? prev.filter((i) => i !== interest)
: [...prev, interest]
);
}
// Send selected interests to the backend for AI analysis
async function analyzeHackathons() {
setLoading(true);
setError(null);
try {
const response = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ interests: selectedInterests }),
});
if (!response.ok) {
throw new Error("Analysis failed. Please try again.");
}
const data = await response.json();
setResults(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
// Reset the dashboard back to interest selection
function resetDashboard() {
setSelectedInterests([]);
setResults(null);
setError(null);
}
return (
<div className="min-h-screen bg-gray-950 text-white">
<nav className="flex items-center justify-between px-8 py-5 max-w-6xl mx-auto">
<button
onClick={() => navigate("/")}
className="text-xl font-bold text-indigo-400 hover:text-indigo-300 transition"
>
HackMatch AI
</button>
{results && (
<button
onClick={resetDashboard}
className="text-sm text-gray-400 hover:text-white transition"
>
Start Over
</button>
)}
</nav>
<div className="max-w-4xl mx-auto px-8 py-10">
{!results && (
<>
<h1 className="text-3xl font-bold mb-2">
What are you interested in?
</h1>
<p className="text-gray-400 mb-8">
Select one or more topics and we will find the best hackathon for
you.
</p>
<div className="flex flex-wrap gap-3 mb-10">
{INTEREST_OPTIONS.map((interest) => (
<button
key={interest}
onClick={() => toggleInterest(interest)}
className={`px-4 py-2 rounded-lg border font-medium transition ${
selectedInterests.includes(interest)
? "bg-indigo-600 border-indigo-500 text-white"
: "bg-gray-900 border-gray-700 text-gray-300 hover:border-gray-500"
}`}
>
{interest}
</button>
))}
</div>
<button
onClick={analyzeHackathons}
disabled={selectedInterests.length === 0 || loading}
className="bg-indigo-600 hover:bg-indigo-500 disabled:bg-gray-700 disabled:text-gray-500 text-white font-semibold px-8 py-3 rounded-lg transition"
>
{loading ? "AI is analyzing hackathons..." : "Analyze Hackathons"}
</button>
{error && (
<p className="mt-4 text-red-400 text-sm">{error}</p>
)}
</>
)}
</div>
</div>
);
}
export default DashboardPage;
What does this code do?
INTEREST_OPTIONS defines the eight categories users can choose from. Each one maps to a hackathon theme in your backend data.
toggleInterest adds or removes an interest from the selectedInterests array. The button styling swaps between indigo (selected) and dark gray (unselected) based on this state.
analyzeHackathons sends a POST request to /api/analyze with the selected interests. The Vite proxy forwards this to your FastAPI backend. While the AI processes, the button text changes to "AI is analyzing hackathons..." so the user knows something is happening.
The {!results && (...)} conditional hides the interest panel once results arrive. The resetDashboard function clears everything so users can start a fresh analysis.
- Save DashboardPage.jsx.
- Make sure your Vite dev server is still running. If it is not, navigate to the frontend folder in your terminal and run:
npm run dev
- Open http://localhost:5173/dashboard in your browser. You should see a heading that says "What are you interested in?" followed by eight clickable interest tags.
- Click a few tags to confirm they toggle between indigo (selected) and dark gray (unselected).
- Verify that the Analyze Hackathons button is grayed out when no tags are selected, and becomes clickable when at least one tag is active.
Tags not toggling or page is blank?
- Check your terminal for a compilation error. A missing curly brace or parenthesis in the JSX is the most common cause.
- Make sure you replaced the entire placeholder file. Any leftover code from the old placeholder can conflict with the new imports.
- Confirm your index.css still contains @import "tailwindcss";. Without it, none of the Tailwind classes will apply.
Help me debug my DashboardPage interest selection panel
Add the score card display with progress bars
When the AI analysis comes back, each hackathon has four scores: prize potential, difficulty, theme match, and team fit. You need a visual way to present these scores. A small reusable component with a labeled progress bar keeps the rendering clean.
- In DashboardPage.jsx, find the closing curly brace and semicolon after INTEREST_OPTIONS. Add the following ScoreBar component below it, before the DashboardPage function:
// Reusable progress bar for a single score dimension
function ScoreBar({ label, score }) {
const percentage = score * 10;
return (
<div className="flex items-center gap-2 text-sm">
<span className="w-28 text-gray-400">{label}</span>
<div className="flex-1 bg-gray-800 rounded-full h-2">
<div
className="bg-indigo-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="w-6 text-right text-gray-300">{score}</span>
</div>
);
}
What does ScoreBar do?
ScoreBar takes a label (like "Prize" or "Theme Match") and a score from 1 to 10. It multiplies the score by 10 to get a percentage width for the indigo fill bar. A score of 8 fills 80% of the bar.
You won't see a change yet because ScoreBar is not used in the JSX. The next chunk adds the results section that renders it.
Now you need a results section that appears after the AI finishes its analysis. Each hackathon gets a card with its name, overall score, and four progress bars.
- In the return statement of DashboardPage, find the closing tag for the {!results && (...)} block. It ends with </> followed by a closing parenthesis and curly brace.
- Directly below that block and still inside the <div className="max-w-4xl ..."> container, add the following results section:
{results && (
<>
<h2 className="text-2xl font-bold mb-6">All Hackathon Scores</h2>
<div className="space-y-4">
{results.scores &&
results.scores
.sort((a, b) => b.overall - a.overall)
.map((item) => (
<div
key={item.hackathon_name}
className="bg-gray-900 border border-gray-800 rounded-xl p-5"
>
{/* Hackathon name and overall score */}
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold">
{item.hackathon_name}
</h3>
<span className="text-indigo-400 font-bold">
{item.overall}/10
</span>
</div>
{/* Four score dimensions as progress bars */}
<div className="space-y-2">
<ScoreBar
label="Prize"
score={item.prize_potential}
/>
<ScoreBar label="Ease" score={item.difficulty} />
<ScoreBar
label="Theme Match"
score={item.theme_match}
/>
<ScoreBar label="Team Fit" score={item.team_fit} />
</div>
</div>
))}
</div>
</>
)}
What does this code do?
The {results && (...)} block only renders when the AI analysis has returned data. It sorts hackathons by their overall score in descending order, so the best-matching hackathon appears first.
Each hackathon renders as a dark card showing four ScoreBar components. The labels ("Prize", "Ease", "Theme Match", "Team Fit") correspond to the four scoring dimensions your backend prompt asks the LLM to evaluate.
- Save DashboardPage.jsx.
- To test this, you need both servers running. Open a second terminal, navigate to the backend folder, activate your virtual environment, and start the FastAPI server:
cd backend
- Activate your virtual environment by running:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
- Set your Ollama API key as an environment variable and start the server by running:
macOS
export OLLAMA_API_KEY=your-api-key-here
uvicorn main:app --reload
Windows
set OLLAMA_API_KEY=your-api-key-here
uvicorn main:app --reload
Replace the API key placeholder
Replace your-api-key-here with the actual API key from your Ollama account. You can find it at ollama.com/settings/keys.
- Go back to your browser at http://localhost:5173/dashboard.
- Select one or two interest tags and click Analyze Hackathons.
- The button text should change to "AI is analyzing hackathons..." while the request processes. After a few seconds, the interest panel disappears and a list of hackathon score cards appears, each with four indigo progress bars.
Seeing an error message or no results?
- Check that your FastAPI server is running on port 8000 and the Vite dev server is running on port 5173. Both must be running at the same time.
- Verify your OLLAMA_API_KEY environment variable is set correctly. If you restarted your terminal, you need to set it again.
- Open the browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for a network error. A 500 status usually means the Ollama API key is missing or invalid.
Help me debug my hackathon analysis API call
Build the top recommendation section
The score cards show every hackathon ranked by overall score. The most valuable part of the AI analysis is the top pick: a single recommended hackathon with a generated project idea, tech stack, and pitch summary. This section gives it a distinct visual treatment so it stands out.
- In DashboardPage.jsx, find the line inside the {results && (...)} block that says:
<h2 className="text-2xl font-bold mb-6">All Hackathon Scores</h2>
- Add the following top pick section directly above that <h2> line, right after the opening <> fragment tag:
{/* Highlighted top recommendation from the AI */}
{results.top_pick && (
<div className="mb-10 bg-indigo-950 border border-indigo-700 rounded-xl p-6">
<div className="flex items-center gap-2 mb-4">
<span className="bg-indigo-600 text-xs font-bold px-2 py-1 rounded">
TOP PICK
</span>
<h2 className="text-xl font-bold">
{results.top_pick.hackathon_name}
</h2>
</div>
<div className="space-y-4">
{/* AI-generated project idea */}
<div>
<h3 className="text-sm font-semibold text-indigo-300 mb-1">
Project Idea
</h3>
<p className="text-gray-300 text-sm">
{results.top_pick.project_idea}
</p>
</div>
{/* Recommended technologies */}
<div>
<h3 className="text-sm font-semibold text-indigo-300 mb-1">
Recommended Tech Stack
</h3>
<div className="flex flex-wrap gap-2">
{results.top_pick.tech_stack.map((tech) => (
<span
key={tech}
className="bg-indigo-900 text-indigo-200 text-xs px-2 py-1 rounded"
>
{tech}
</span>
))}
</div>
</div>
{/* Elevator pitch for the top hackathon */}
<div>
<h3 className="text-sm font-semibold text-indigo-300 mb-1">
Pitch Summary
</h3>
<p className="text-gray-300 text-sm italic">
{results.top_pick.pitch_summary}
</p>
</div>
</div>
</div>
)}
What does this code do?
This section only renders if the AI returned a top_pick in the response. It gets a distinctive indigo background and border to visually separate it from the score cards below.
The "TOP PICK" badge uses a small indigo pill to label the recommendation. The tech stack renders each technology as an individual badge using .map(), and the pitch summary is styled in italic to set it apart as a generated quote.
- Save DashboardPage.jsx.
- Refresh the browser at http://localhost:5173/dashboard and run another analysis by selecting interests and clicking Analyze Hackathons.
- You should see a prominent indigo card at the top with a "TOP PICK" badge, the recommended hackathon name, a project idea paragraph, tech stack badges, and an italic pitch summary. Below it, all the score cards with progress bars should appear as before.
- Click the Start Over link in the top right to confirm it resets back to the interest selection panel.
Top pick section not showing?
- Make sure the top pick JSX is placed inside the {results && (...)} block, before the "All Hackathon Scores" heading. If it is outside that block, it will try to access results.top_pick when results is null.
- Check the browser console for errors. A Cannot read properties of undefined error usually means the AI response did not include a top_pick field. Try running the analysis again.
Help me fix my top pick recommendation display
✔️ Awesome, I've got everything!
Great. Double check you have saved DashboardPage.jsx before moving on.
ⓧ I'd like to double check the full code
Here is the complete DashboardPage.jsx file. Compare it against yours to spot any differences.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
const INTEREST_OPTIONS = [
"AI/ML",
"Web3",
"Healthcare",
"Fintech",
"SaaS",
"Gaming",
"Climate",
"Education",
];
function ScoreBar({ label, score }) {
const percentage = score * 10;
return (
<div className="flex items-center gap-2 text-sm">
<span className="w-28 text-gray-400">{label}</span>
<div className="flex-1 bg-gray-800 rounded-full h-2">
<div
className="bg-indigo-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="w-6 text-right text-gray-300">{score}</span>
</div>
);
}
function DashboardPage() {
const navigate = useNavigate();
const [selectedInterests, setSelectedInterests] = useState([]);
const [results, setResults] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
function toggleInterest(interest) {
setSelectedInterests((prev) =>
prev.includes(interest)
? prev.filter((i) => i !== interest)
: [...prev, interest]
);
}
async function analyzeHackathons() {
setLoading(true);
setError(null);
try {
const response = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ interests: selectedInterests }),
});
if (!response.ok) {
throw new Error("Analysis failed. Please try again.");
}
const data = await response.json();
setResults(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
function resetDashboard() {
setSelectedInterests([]);
setResults(null);
setError(null);
}
return (
<div className="min-h-screen bg-gray-950 text-white">
<nav className="flex items-center justify-between px-8 py-5 max-w-6xl mx-auto">
<button
onClick={() => navigate("/")}
className="text-xl font-bold text-indigo-400 hover:text-indigo-300 transition"
>
HackMatch AI
</button>
{results && (
<button
onClick={resetDashboard}
className="text-sm text-gray-400 hover:text-white transition"
>
Start Over
</button>
)}
</nav>
<div className="max-w-4xl mx-auto px-8 py-10">
{!results && (
<>
<h1 className="text-3xl font-bold mb-2">
What are you interested in?
</h1>
<p className="text-gray-400 mb-8">
Select one or more topics and we will find the best hackathon for
you.
</p>
<div className="flex flex-wrap gap-3 mb-10">
{INTEREST_OPTIONS.map((interest) => (
<button
key={interest}
onClick={() => toggleInterest(interest)}
className={`px-4 py-2 rounded-lg border font-medium transition ${
selectedInterests.includes(interest)
? "bg-indigo-600 border-indigo-500 text-white"
: "bg-gray-900 border-gray-700 text-gray-300 hover:border-gray-500"
}`}
>
{interest}
</button>
))}
</div>
<button
onClick={analyzeHackathons}
disabled={selectedInterests.length === 0 || loading}
className="bg-indigo-600 hover:bg-indigo-500 disabled:bg-gray-700 disabled:text-gray-500 text-white font-semibold px-8 py-3 rounded-lg transition"
>
{loading ? "AI is analyzing hackathons..." : "Analyze Hackathons"}
</button>
{error && (
<p className="mt-4 text-red-400 text-sm">{error}</p>
)}
</>
)}
{results && (
<>
{results.top_pick && (
<div className="mb-10 bg-indigo-950 border border-indigo-700 rounded-xl p-6">
<div className="flex items-center gap-2 mb-4">
<span className="bg-indigo-600 text-xs font-bold px-2 py-1 rounded">
TOP PICK
</span>
<h2 className="text-xl font-bold">
{results.top_pick.hackathon_name}
</h2>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-indigo-300 mb-1">
Project Idea
</h3>
<p className="text-gray-300 text-sm">
{results.top_pick.project_idea}
</p>
</div>
<div>
<h3 className="text-sm font-semibold text-indigo-300 mb-1">
Recommended Tech Stack
</h3>
<div className="flex flex-wrap gap-2">
{results.top_pick.tech_stack.map((tech) => (
<span
key={tech}
className="bg-indigo-900 text-indigo-200 text-xs px-2 py-1 rounded"
>
{tech}
</span>
))}
</div>
</div>
<div>
<h3 className="text-sm font-semibold text-indigo-300 mb-1">
Pitch Summary
</h3>
<p className="text-gray-300 text-sm italic">
{results.top_pick.pitch_summary}
</p>
</div>
</div>
</div>
)}
<h2 className="text-2xl font-bold mb-6">All Hackathon Scores</h2>
<div className="space-y-4">
{results.scores &&
results.scores
.sort((a, b) => b.overall - a.overall)
.map((item) => (
<div
key={item.hackathon_name}
className="bg-gray-900 border border-gray-800 rounded-xl p-5"
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold">
{item.hackathon_name}
</h3>
<span className="text-indigo-400 font-bold">
{item.overall}/10
</span>
</div>
<div className="space-y-2">
<ScoreBar
label="Prize"
score={item.prize_potential}
/>
<ScoreBar label="Ease" score={item.difficulty} />
<ScoreBar
label="Theme Match"
score={item.theme_match}
/>
<ScoreBar label="Team Fit" score={item.team_fit} />
</div>
</div>
))}
</div>
</>
)}
</div>
</div>
);
}
export default DashboardPage;
Your dashboard is fully functional. Users can select interests, get AI-powered scores, and see a top recommendation with a ready-made project pitch. Next up, you will connect both servers together for a final end-to-end test and add some UX polish to make the experience feel complete.
Connect Everything and Polish
In the previous steps, you built every piece of the application separately. The FastAPI backend has endpoints for hackathon data and AI analysis. The React frontend has a landing page and an interactive dashboard.
Now it is time to bring the two halves together. You will set your Ollama API key, start both servers, and walk through the full user flow from landing page to AI-scored results.
In this step, get ready to:
- Set your Ollama API key and start both servers.
- Test the full user flow end to end and verify error handling.
Set your API key and start both servers
Your backend code reads the OLLAMA_API_KEY environment variable when it starts. You need to set this in the same terminal session before launching the server.
- Make sure your terminal is inside the backend folder with your virtual environment activated.
- Set your Ollama API key by running this command (replace the placeholder with your actual key from ollama.com/settings/keys):
macOS
export OLLAMA_API_KEY=your-api-key-here
Windows
If you are using Command Prompt:
set OLLAMA_API_KEY=your-api-key-here
If you are using PowerShell:
$env:OLLAMA_API_KEY="your-api-key-here"
Where do I get my API key?
Ollama Cloud gives you a free API key at ollama.com/settings/keys. The free tier allows light usage with 1 concurrent cloud model.
Sessions reset every 5 hours and weekly limits reset every 7 days. This is plenty for development and testing.
- Start the FastAPI backend by running this command:
uvicorn main:app --reload
What does this command do?
Uvicorn is the web server bundled with fastapi[standard]. The --reload flag tells it to restart automatically whenever you save a file.
- You should see output that includes Uvicorn running on http://127.0.0.1:8000.
Seeing an error when starting uvicorn?
- If you see ModuleNotFoundError, your virtual environment may not be activated. Run the activation command again from inside the backend folder.
- If port 8000 is already in use, close any other terminal sessions running uvicorn.
- Help me debug my uvicorn startup error
Now you need a second terminal for the frontend. Keep the backend terminal running.
- Open a new terminal window.
- Navigate to the frontend folder inside your hackathon-discovery project by running:
cd ~/Desktop/hackathon-discovery/frontend
- Start the Vite dev server by running:
npm run dev
- You should see output that includes Local: http://localhost:5173/.
Why two separate servers?
The Vite dev server on port 5173 serves your React frontend. The FastAPI server on port 8000 handles your API and AI logic.
The proxy you configured in vite.config.js automatically forwards any request starting with /api from the frontend to the backend. Your React code calls /api/analyze and Vite routes it to http://localhost:8000/api/analyze behind the scenes.
Vite dev server not starting?
- Make sure you are inside the frontend folder, not the project root.
- If you see missing dependency errors, run npm install first.
- Help me fix my Vite dev server startup error
Test the full user flow end to end
Both servers are running. Time to walk through the entire application like a real user would.
- Open your browser and go to http://localhost:5173.
You should see the HackMatch AI landing page with a large headline, a subtitle, and a Get Started Free button. Three feature cards appear below the hero section.
- Click the Get Started Free button.
The browser navigates to /dashboard. You should see a grid of eight interest tags and a disabled Analyze Hackathons button.
- Click two or three interest tags that appeal to you.
Each tag you click turns indigo to show it is selected. The Analyze Hackathons button becomes active as soon as at least one tag is selected.
- Click Analyze Hackathons.
The button text changes to "AI is analyzing hackathons..." while the request is in progress. This may take 10-30 seconds depending on the LLM response time.
What is happening behind the scenes?
Your React frontend sends a POST request to /api/analyze with your selected interests. Vite's proxy forwards this to the FastAPI backend on port 8000.
The backend builds a structured prompt, sends it to the Ollama Cloud API, parses the JSON response, and returns scored hackathon results to the frontend.
Once the analysis completes, you should see two sections appear on the dashboard.
- Verify that a TOP PICK card appears at the top with an indigo border. It displays the recommended hackathon name, a project idea, a tech stack with individual tags, and a pitch summary.
- Scroll down to the All Hackathon Scores section. Each hackathon appears as a card with four score bars: Prize, Ease, Theme Match, and Team Fit.
Scores vary between runs
The LLM generates slightly different scores each time. This is normal. The overall ranking should stay consistent for interests that closely match a hackathon's theme.
- Click the Start Over link in the top-right corner of the navigation bar.
The dashboard resets back to the interest selection view. All tags are deselected and the results are cleared.
- Click the HackMatch AI logo in the top-left corner.
You should navigate back to the landing page. The full loop from landing page to dashboard to results and back works.
Seeing an error message on the dashboard?
- If you see "Analysis failed", check that your OLLAMA_API_KEY environment variable is set in the terminal running uvicorn. The key must be set before you start the server.
- If the request hangs forever, confirm the FastAPI server is still running in the other terminal. Look for Uvicorn running on http://127.0.0.1:8000 in the output.
- If you see a CORS error in the browser console, make sure both servers are running on the correct ports (backend on 8000, frontend on 5173).
- Help me debug why my hackathon analysis request is failing
Secret mission
Compare Hackathons Side-by-Side
Your dashboard scores and ranks hackathons, but what if a user is torn between two options? In this extension, you'll add a side-by-side comparison feature. Users select two hackathons, click Compare, and the LLM generates a detailed pros/cons analysis with a final recommendation.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with the free Ollama Cloud tier, so there are no ongoing costs.
Resources you used:
- FastAPI backend server (uvicorn on port 8000).
- Vite dev server (port 5173).
- Python virtual environment in backend/venv.
- Node.js packages in frontend/node_modules.
- Project files in the hackathon-discovery folder on your Desktop.
- Ollama Cloud API key (managed at ollama.com/settings/keys).
Keep everything running
No action needed. Choose this if you are still actively building or want to extend the project with the Secret Mission.
- Your FastAPI and Vite dev servers will keep running until you close the terminal windows.
- Your Ollama Cloud API key stays active on the free tier.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files stay on disk so you can pick up where you left off.
- In each terminal window running a server, press Ctrl+C to stop the process.
- Deactivate your Python virtual environment by typing deactivate in the backend terminal.
- When you come back, reactivate the virtual environment and restart both servers.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone.
- Stop both dev servers by pressing Ctrl+C in each terminal window.
- Delete the entire hackathon-discovery folder from your Desktop.
- If you want to revoke your Ollama Cloud API key, visit ollama.com/settings/keys and delete the key you created for this project.
Mission accomplished
That's a Wrap!
Nice work! You just built a full-stack AI-powered web app from scratch. You have a professional SaaS-style Hackathon Discovery Assistant that uses an LLM to analyze, score, and recommend hackathons based on a user's interests.
You've learned how to:
- Built a React single-page application with a landing page and interactive dashboard, styled with Tailwind CSS and scaffolded using Vite.
- Created a FastAPI backend with REST API endpoints that connect to Ollama's Cloud LLM for intelligent hackathon analysis.
- Implemented a multi-dimensional scoring engine that evaluates hackathons on prize potential, difficulty, theme match, and team fit. The top pick comes with an AI-generated project idea, tech stack, and pitch summary.
- Added a side-by-side hackathon comparison feature that sends two hackathons to the LLM and displays a detailed pros and cons analysis.
Ready to quiz yourself?