Search live jobs by role and city, filter by employment type, and click a card for a plain-English Gemini summary. Apply opens the listing in a new tab.
The frontend and API run as one Next.js app on Vercel.
| Layer | Tech |
|---|---|
| Framework | Next.js (App Router) + TypeScript |
| Styling | Tailwind CSS v4 (theme tokens in globals.css) |
| API | Next.js route handlers (src/app/api) |
| Jobs | JSearch (RapidAPI) |
| AI | Google Gemini (one job per card click) |
| Deploy | Vercel |
SearchBar → Header → page.tsx → GET /api/jobs → jsearch → JobList
- Role + city become one query string (
developer in Miami). - Employment type is a separate query param (
FULLTIME,PARTTIME,CONTRACTOR,INTERN, orall). - Successful searches are saved in the browser (
localStorage) as recent criteria (role, city, filter) — not job results or API keys. Focus the role field to pick one again, or clear the list. - Click a card (not Apply) for
POST /api/analyze. - Card summary:
JobCard->POST /api/analyze->gemini.ts-> summary on the card.
jobify/
├── src/
│ ├── app/
│ │ ├── page.tsx # Home: search state + results
│ │ ├── about/page.tsx # About page
│ │ ├── layout.tsx # Root layout, footer, Poppins font
│ │ ├── globals.css # Tailwind + theme tokens (@theme)
│ │ └── api/
│ │ ├── jobs/route.ts # GET /api/jobs
│ │ └── analyze/route.ts # POST /api/analyze
│ ├── components/
│ │ ├── ui/ # Button, Input, Card
│ │ ├── Footer.tsx
│ │ └── Header, SearchBar, FilterPanel, JobCard, JobList
│ ├── hooks/
│ │ └── useAsync.ts # Shared loading/error state for async UI
│ ├── lib/
│ │ ├── fetchJobs.ts # Browser fetch helpers for /api/*
│ │ ├── RecentSearches.ts # localStorage recent search history
│ │ ├── jsearch.ts # JSearch API call (server only)
│ │ └── gemini.ts # Gemini API call (server only)
│ └── types/ # Job, JSearchJob, AIAnalysis, RecentSearch, EmploymentType
├── .env.example
└── package.json
- Node.js 20.9 or newer
- A JSearch RapidAPI key
- A Google Gemini key (for card summaries)
npm installCopy .env.example to .env.local in the project root:
JSEARCH_API_KEY=your_jsearch_key
GEMINI_API_KEY=your_gemini_keynpm run devApp and API: http://localhost:3000.
The keys are only read on the server, so they never reach the browser.
- Search by role and city
- Filter employment type, then Search
- Recent searches on the role field (rerun or clear; stored on this device only, no login)
- Loading, failed, and empty states
- Click a card for a Gemini summary (skills + salary if the model finds one)
- Apply opens the job without starting a summary
- About (
/about) and footer links on every page
There is no salary filter. JSearch often leaves salary empty.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/jobs?query=...&employmentType=... |
JSearch listings. employmentType of all skips the type filter. |
POST |
/api/analyze |
Gemini summary. Body: { title, description }. |
Vercel builds from main. In the Vercel project settings, set the Framework Preset to Next.js, leave Root Directory empty, and add JSEARCH_API_KEY and GEMINI_API_KEY under Environment Variables.
After you add or change environment variables, redeploy so production picks them up.
On every push and pull request to main, GitHub Actions installs dependencies, runs ESLint, and runs a production build. It does not call JSearch or Gemini.
| Script | What it does |
|---|---|
npm run dev |
Next.js dev server |
npm run build |
Production build |
npm start |
Run the production build |
npm run lint |
ESLint |
- Zod validation for
/api/jobsand/api/analyze - Rate limit on
POST /api/analyze - Tune JSearch timeouts when RapidAPI is slow
- Unit tests (JSearch mapper, API routes)
- Server vs Client Components in Next.js
ISC