Your intelligent career co-pilot — powered by Google Gemini AI
- Overview
- Live Features
- System Design & Architecture
- Request Lifecycle
- Core User Flows
- Background Job Flow
- Tech Stack & Why We Chose It
- Project Structure
- File-by-File Pseudocode & Key Functions
- Database Schema
- Environment Variables
- Getting Started
- Deployment
AI Career Coach is a full-stack, AI-powered career acceleration platform. It combines Google's Gemini LLM, a PostgreSQL database, serverless background jobs, and a beautiful Next.js frontend to give professionals an unfair advantage in their careers.
The platform acts as a personal career strategist — it generates tailored interview quizzes, builds ATS-optimized resumes, drafts personalized cover letters, and provides real-time industry insights.
| Feature | Description |
|---|---|
| 🎯 AI Interview Prep | Generates 10 industry-specific MCQ questions via Gemini AI with instant feedback & explanations |
| 📝 Smart Resume Builder | Markdown-based resume editor with AI section improvement powered by Gemini |
| 💌 Cover Letter Generator | Creates tailored, professional cover letters from job description input |
| 📊 Career Dashboard | Real-time industry insights: salary ranges, demand levels, key trends, and growth rates |
| 🔐 Secure Auth | Full Clerk authentication with protected routes and session management |
| 🔄 Auto-Updated Insights | Weekly background job (Inngest cron) refreshes all industry data automatically |
| 🌗 Dark / Light Mode | Fully themed UI with next-themes |
| 📄 PDF Export | Export your resume as a PDF with html2pdf.js |
flowchart TB
subgraph Client["🖥️ CLIENT BROWSER"]
Hero["Hero / Landing"]
Dash["Dashboard Page"]
Quiz["Interview Quiz Page"]
Resume["Resume Builder"]
Cover["Cover Letter Generator"]
end
subgraph NextJS["⚡ NEXT.JS 15 APP ROUTER (SSR + RSC)"]
MW["middleware.js<br/>Clerk route guard"]
subgraph Actions["Server Actions ('use server')"]
UserAction["user.js"]
InterviewAction["interview.js"]
CoverAction["cover-letter.js"]
ResumeAction["resume.js"]
DashAction["dashboard.js"]
end
end
ClerkAuth["🔐 CLERK AUTH<br/>JWT Sessions + Middleware"]
Gemini["🤖 GOOGLE GEMINI AI<br/>gemini-2.5-flash<br/>• Quiz Generation<br/>• Resume Improvement<br/>• Cover Letter Gen<br/>• Industry Insights"]
Prisma["🗄️ PRISMA ORM<br/>Type-safe query builder + migrations"]
subgraph DB["🐘 POSTGRESQL DATABASE"]
UserT[(User)]
AssessT[(Assessment)]
ResumeT[(Resume)]
CoverT[(CoverLetter)]
InsightT[(IndustryInsight)]
end
subgraph Inngest["⏱️ INNGEST — Background Jobs"]
CronJob["generateIndustryInsights()<br/>Cron: Every Sunday @ midnight"]
end
Hero --> NextJS
Dash --> NextJS
Quiz --> NextJS
Resume --> NextJS
Cover --> NextJS
MW --> ClerkAuth
Actions --> Gemini
Actions --> Prisma
Prisma --> DB
CronJob -->|"fetch industries"| DB
CronJob -->|"generate fresh insights"| Gemini
CronJob -->|"update records"| InsightT
style Client fill:#1e293b,color:#fff
style NextJS fill:#0f172a,color:#fff
style ClerkAuth fill:#6C47FF,color:#fff
style Gemini fill:#4285F4,color:#fff
style Prisma fill:#2D3748,color:#fff
style DB fill:#336791,color:#fff
style Inngest fill:#FF6C37,color:#fff
How a single authenticated request flows through the stack, from browser click to database write.
sequenceDiagram
actor User
participant MW as middleware.js
participant Clerk as Clerk Auth
participant Page as App Router Page
participant SA as Server Action
participant Gemini as Gemini AI
participant DB as PostgreSQL (Prisma)
User->>MW: Request protected route (e.g. /dashboard)
MW->>Clerk: Verify session (JWT)
alt Not authenticated
Clerk-->>User: Redirect to /sign-in
else Authenticated
Clerk-->>MW: userId
MW->>Page: Allow request
Page->>SA: Call server action (e.g. getIndustryInsights)
SA->>DB: Query via Prisma
alt Cache miss / needs AI
SA->>Gemini: generateContent(prompt)
Gemini-->>SA: Structured JSON response
SA->>DB: Persist result
end
DB-->>SA: Data
SA-->>Page: Response
Page-->>User: Rendered UI
end
sequenceDiagram
actor User
participant UI as Quiz Component
participant Hook as useFetch(generateQuiz)
participant SA as actions/interview.js
participant Gemini as Gemini AI
participant DB as PostgreSQL
User->>UI: Click "Start Quiz"
UI->>Hook: fn()
Hook->>SA: generateQuiz()
SA->>DB: Fetch user.industry + user.skills
SA->>Gemini: Prompt for 10 MCQs
Gemini-->>SA: JSON {questions[]}
SA-->>UI: questions[]
loop For each question
User->>UI: Select answer
UI->>UI: Compare vs correctAnswer, show feedback
end
User->>UI: Finish quiz
UI->>SA: saveQuizResult(questions, answers, score)
SA->>Gemini: Generate improvement tip (if wrong answers)
Gemini-->>SA: improvementTip
SA->>DB: db.assessment.create(...)
SA-->>UI: Saved result
UI-->>User: Show <QuizResult /> with score & trend
sequenceDiagram
actor User
participant UI as Cover Letter Form
participant SA as actions/cover-letter.js
participant Gemini as Gemini AI
participant DB as PostgreSQL
User->>UI: Enter jobTitle, companyName, jobDescription
UI->>SA: generateCoverLetter(data)
SA->>DB: Fetch user profile (industry, experience, skills, bio)
SA->>Gemini: Context-rich prompt (profile + job + rules)
Gemini-->>SA: Markdown cover letter (≤400 words)
SA->>DB: db.coverLetter.create(...)
SA-->>UI: Saved cover letter
UI-->>User: Render markdown letter
sequenceDiagram
actor User
participant UI as Resume Builder
participant SA as actions/resume.js
participant Gemini as Gemini AI
participant DB as PostgreSQL
User->>UI: Click "Improve with AI" on a section
UI->>SA: improveWithAI({ current, type })
SA->>DB: Fetch user + industryInsight
SA->>Gemini: "Improve {type} section for {industry} pro..."
Gemini-->>SA: Improved paragraph
SA-->>UI: Return improved text
User->>UI: Click "Save"
UI->>SA: saveResume(content)
SA->>DB: db.resume.upsert({ userId })
generateIndustryInsights runs automatically every Sunday at midnight via Inngest's durable cron — no separate worker server required.
flowchart LR
Cron["⏰ Cron Trigger<br/>0 0 * * 0 (Sun @ 00:00)"] --> Fetch["Step 1: Fetch Industries<br/>db.industryInsight.findMany()"]
Fetch --> Loop{"For each<br/>industry"}
Loop --> AICall["Step 2: step.ai.wrap()<br/>Gemini prompt for industry analysis"]
AICall --> Parse["Parse JSON:<br/>salaryRanges, growthRate,<br/>demandLevel, topSkills,<br/>marketOutlook, keyTrends"]
Parse --> Update["Step 3: Update DB<br/>lastUpdated = now()<br/>nextUpdate = now() + 7d"]
Update --> Loop
Loop -->|done| End(["✅ All industries refreshed"])
style Cron fill:#FF6C37,color:#fff
style AICall fill:#4285F4,color:#fff
style Update fill:#336791,color:#fff
| Technology | Version | Why It Matters |
|---|---|---|
| Next.js | ^15.5.9 |
App Router enables server components, server actions, and streaming. Turbopack provides blazing-fast HMR. Eliminates the need for a separate Express backend. |
| React | ^18.3.1 |
Concurrent rendering with Suspense and useTransition enables smooth UX during AI generation waits. |
| Technology | Version | Why It Matters |
|---|---|---|
| @google/generative-ai | ^0.21.0 |
Direct SDK access to Gemini 2.5 Flash — Google's fastest model for structured JSON output. Powers quiz generation, resume improvement, cover letters, and industry analysis. |
| Technology | Version | Why It Matters |
|---|---|---|
| @clerk/nextjs | ^6.9.10 |
Drop-in auth with JWT session management, social logins, and pre-built UI. Middleware integration protects entire route groups in a single file. |
| @clerk/themes | ^2.2.5 |
Syncs Clerk's modal UI with the app's dark/light theme seamlessly. |
| Technology | Version | Why It Matters |
|---|---|---|
| PostgreSQL | Latest | Relational DB with JSONB support — perfect for mixed structured (User, Resume) and semi-structured (questions[], salaryRanges[]) data. |
| Prisma | ^6.2.1 |
Type-safe ORM with auto-generated client, schema-first migrations, and $transaction() for ACID-compliant multi-step operations. |
| Technology | Version | Why It Matters |
|---|---|---|
| Inngest | ^3.54.0 |
Serverless-native event-driven job queue with built-in cron scheduling. Runs the weekly industry insight update without a separate worker server. step.ai.wrap() enables durable AI calls that survive failures. |
| Technology | Version | Why It Matters |
|---|---|---|
| TailwindCSS | ^3.4.1 |
Utility-first CSS for rapid, consistent design directly in JSX. |
| Radix UI | Various | Headless, accessible component primitives (Dialog, Tabs, Accordion) styled with Tailwind. Zero accessibility debt. |
| Framer Motion | ^12.23.26 |
Production-grade animations — parallax scrolling, staggered reveals, and the typing cursor in the hero. |
| shadcn/ui | — | Component library built on Radix + Tailwind for consistent Button, Card, Badge, and form components. |
| Lucide React | ^0.471.1 |
Crisp, consistent SVG icon set. |
| next-themes | ^0.4.4 |
Zero-flash dark/light mode switching integrated with Tailwind's dark variant. |
| Technology | Version | Why It Matters |
|---|---|---|
| React Hook Form | ^7.54.2 |
Performant form management with minimal re-renders — critical for the multi-section onboarding flow. |
| Zod | ^3.24.1 |
Schema-first validation — validates form inputs on the client before hitting server actions. |
| @hookform/resolvers | ^3.10.0 |
Bridges Zod schemas directly into React Hook Form without custom validators. |
| Technology | Version | Why It Matters |
|---|---|---|
| Recharts | ^2.15.0 |
Composable charts for salary range bar charts and quiz performance trend lines on the dashboard. |
| Technology | Version | Why It Matters |
|---|---|---|
| html2pdf.js | ^0.10.2 |
Client-side PDF generation from resume HTML content — no server-side rendering required. |
| react-markdown | ^9.0.3 |
Renders Gemini's Markdown output (cover letters, resume sections) as formatted HTML. |
| @uiw/react-md-editor | ^4.0.5 |
Full-featured Markdown editor for the resume builder with live preview. |
| date-fns | ^4.1.0 |
Lightweight date utility for formatting timestamps. |
| sonner | ^1.7.1 |
Beautiful, accessible toast notifications for quiz feedback and save confirmations. |
| react-spinners | ^0.15.0 |
Loading indicators (BarLoader) shown during AI generation. |
| clsx + tailwind-merge | — | Compose conditional Tailwind class names safely without conflicts. |
ai-career-coach/
│
├── 📁 app/ # Next.js App Router
│ ├── 📁 (auth)/ # Auth route group (Clerk sign-in/sign-up)
│ ├── 📁 (main)/ # Protected main app routes
│ │ ├── 📁 dashboard/ # Career insights dashboard
│ │ ├── 📁 interview/ # AI quiz + mock interview
│ │ │ └── 📁 _components/ # Quiz, QuizResult, StatsCards
│ │ ├── 📁 resume/ # Resume builder
│ │ │ └── 📁 _components/ # ResumeBuilder, EntryForm
│ │ ├── 📁 ai-cover-letter/ # Cover letter generator & list
│ │ ├── 📁 onboarding/ # First-login industry/skill setup
│ │ └── 📁 settings/ # User profile settings
│ ├── 📁 api/
│ │ └── 📁 inngest/ # Inngest webhook endpoint
│ ├── layout.js # Root layout + ThemeProvider + Clerk
│ ├── page.js # Landing page
│ └── globals.css # Global styles + dot-grid + gradient-title
│
├── 📁 actions/ # Next.js Server Actions ("use server")
│ ├── cover-letter.js # CRUD for cover letters
│ ├── dashboard.js # Industry insights generation
│ ├── interview.js # Quiz generation + assessment saving
│ ├── resume.js # Resume save/get/AI-improve
│ └── user.js # User profile + onboarding status
│
├── 📁 components/ # Shared UI components
│ ├── header.jsx # Navbar with auth + theme toggle
│ ├── hero.jsx # Landing hero with typing animation
│ ├── theme-provider.jsx # next-themes wrapper
│ └── 📁 ui/ # shadcn/ui primitives
│
├── 📁 hooks/
│ └── use-fetch.js # Universal async data-fetching hook
│
├── 📁 lib/
│ ├── checkUser.js # Clerk → Prisma user sync
│ ├── prisma.js # Prisma client singleton
│ ├── utils.js # cn() utility
│ └── 📁 inngest/
│ ├── client.js # Inngest client init
│ └── function.js # generateIndustryInsights cron job
│
├── 📁 prisma/
│ ├── schema.prisma # DB schema (User, Assessment, Resume...)
│ └── 📁 migrations/ # Migration history
│
├── 📁 data/ # Static data (industry lists, etc.)
├── middleware.js # Clerk auth middleware + route protection
├── next.config.mjs # Next.js config
├── tailwind.config.mjs # Tailwind theme config
└── package.json # Dependencies & scripts
Purpose: Intercepts every request and enforces authentication on protected routes.
FUNCTION: clerkMiddleware (default export)
INPUT: auth context, incoming request (req)
1. Define protected routes:
[/dashboard, /resume, /interview, /ai-cover-letter, /onboarding]
2. Extract userId from Clerk auth session
3. IF userId is null AND route is protected:
→ Redirect to Clerk sign-in page
4. ELSE → Allow request to proceed (NextResponse.next)
🔑 KEY FUNCTIONS:
createRouteMatcher([...routes]) — builds URL matcher for protected paths
clerkMiddleware(handler) — wraps Next.js middleware with Clerk context
auth().redirectToSignIn() — redirects unauthenticated users
Purpose: On every authenticated request, ensures the Clerk user exists in PostgreSQL.
FUNCTION: checkUser()
1. Call Clerk's currentUser() to get auth session
2. IF no user → return null (guest/public page)
3. QUERY db.user WHERE clerkUserId = user.id
4. IF found → return existing DB user
5. IF NOT found (first login):
→ CREATE new User record with: { clerkUserId, name, imageUrl, email }
→ Return newly created user
🔑 KEY FUNCTIONS:
currentUser() — Clerk server-side session getter
db.user.findUnique() — Prisma query by clerkUserId
db.user.create() — Provision new user in DB on first login
Purpose: Creates a single, reused Prisma client to prevent connection pool exhaustion in serverless environments.
PATTERN: Global Singleton
IF global.prisma already exists:
→ Reuse the existing PrismaClient instance
ELSE:
→ Create new PrismaClient()
→ Store on globalThis to survive hot-reloads
EXPORT: db (the singleton PrismaClient)
🔑 KEY PATTERN:
globalThis.prisma ?? new PrismaClient() — prevents N+1 connections in dev
Purpose: Runs every Sunday at midnight to refresh industry insights for all industries via Gemini AI.
FUNCTION: generateIndustryInsights
TRIGGER: cron("0 0 * * 0") — Every Sunday, midnight
STEP 1 — "Fetch industries":
→ db.industryInsight.findMany()
→ Returns list of all tracked industry strings
FOR EACH industry in industries:
STEP 2 — AI Generation (step.ai.wrap):
→ Build Gemini prompt for industry analysis
→ Call model.generateContent(prompt)
→ Parse JSON response:
{ salaryRanges, growthRate, demandLevel,
topSkills, marketOutlook, keyTrends, recommendedSkills }
STEP 3 — "Update {industry} insights":
→ db.industryInsight.update()
→ Set lastUpdated = now()
→ Set nextUpdate = now() + 7 days
🔑 KEY FUNCTIONS:
inngest.createFunction() — registers the background job
step.run("label", fn) — durable step execution (survives crashes)
step.ai.wrap("gemini", fn, p) — AI call with Inngest retry layer
model.generateContent(prompt) — Gemini API call
db.industryInsight.update() — Prisma update of cached insights
Purpose: Handles user onboarding profile updates and status checks using Prisma transactions.
FUNCTION: updateUser(data)
1. auth() → get userId from Clerk
2. db.user.findUnique(clerkUserId)
3. db.$transaction([
a. Check if IndustryInsight exists for industry
b. IF NOT → generateAIInsights(industry)
→ db.industryInsight.create(...)
c. db.user.update({ industry, experience, bio, skills })
], { timeout: 10000 })
4. revalidatePath("/") → clear Next.js page cache
────────────────────────────────────────────────────
FUNCTION: getUserOnboardingStatus()
1. auth() → userId
2. db.user.findUnique(select: { industry })
3. RETURN { isOnboarded: !!user.industry }
────────────────────────────────────────────────────
FUNCTION: getUser()
1. auth() → userId
2. db.user.findUnique(clerkUserId)
3. RETURN full user object
🔑 KEY FUNCTIONS:
db.$transaction(fn, { timeout }) — ACID-safe multi-step DB operation
generateAIInsights(industry) — Gemini-powered industry data generator
revalidatePath("/") — Invalidates Next.js cache after mutation
Purpose: Fetches or lazily generates industry insight data for the logged-in user's career field.
FUNCTION: generateAIInsights(industry)
1. Build structured Gemini prompt requesting:
{ salaryRanges[], growthRate, demandLevel,
topSkills[], marketOutlook, keyTrends[], recommendedSkills[] }
2. model.generateContent(prompt)
3. Strip markdown code fences from response
4. JSON.parse → return structured insight object
────────────────────────────────────────────────────
FUNCTION: getIndustryInsights()
1. auth() → userId
2. db.user.findUnique({ include: { industryInsight } })
3. IF insight exists → return it (CACHE HIT)
4. IF NOT (cache miss):
→ generateAIInsights(user.industry)
→ db.industryInsight.create(insights)
→ Set nextUpdate = now() + 7 days
→ return new record
🔑 KEY FUNCTIONS:
generateAIInsights(industry) — Gemini-powered insight generation
getIndustryInsights() — Lazy-cache pattern for DB + AI
text.replace(/```(?:json)?\n?/g, "") — Strips AI markdown fences from JSON
Purpose: Generates personalized quiz questions via Gemini, saves quiz results, and fetches assessment history.
FUNCTION: generateQuiz()
1. auth() → userId
2. db.user.findUnique(select: { industry, skills })
3. Build prompt:
"Generate 10 MCQ questions for a {industry} professional
with expertise in {skills}"
Expected JSON: { questions: [{question, options[], correctAnswer, explanation}] }
4. model.generateContent(prompt)
5. Parse and clean JSON response
6. RETURN questions[]
────────────────────────────────────────────────────
FUNCTION: saveQuizResult(questions, answers, score)
1. auth() → userId
2. Map questions → questionResults[]
{ question, answer, userAnswer, isCorrect }
3. Filter wrong answers
4. IF wrongAnswers.length > 0:
→ Build improvementPrompt with wrong Q&A pairs
→ model.generateContent(improvementPrompt)
→ Extract improvementTip string
5. db.assessment.create({
userId, quizScore, questions, category: "Technical", improvementTip })
────────────────────────────────────────────────────
FUNCTION: getAssessments()
1. auth() → userId
2. db.assessment.findMany(orderBy: createdAt ASC)
3. RETURN assessments[] for charting trend lines
🔑 KEY FUNCTIONS:
generateQuiz() — AI-powered personalized quiz creator
saveQuizResult(q, a, score) — Persists results + generates AI tips
getAssessments() — Fetches history for trend charts
model.generateContent(prompt) — Gemini 2.5 Flash API call
Purpose: Handles resume CRUD and AI-powered section content improvement.
FUNCTION: saveResume(content)
1. auth() → userId
2. db.resume.upsert({
where: { userId },
update: { content },
create: { userId, content }
}) ← One resume per user guaranteed
3. revalidatePath("/resume")
────────────────────────────────────────────────────
FUNCTION: getResume()
1. auth() → userId
2. db.resume.findUnique(userId)
3. RETURN resume or null
────────────────────────────────────────────────────
FUNCTION: improveWithAI({ current, type })
1. auth() → userId
2. db.user.findUnique(include: industryInsight)
3. Build expert resume writer prompt:
"Improve this {type} section for a {industry} professional.
Use action verbs, quantify achievements, add industry keywords."
4. model.generateContent(prompt)
5. RETURN improved text (single paragraph)
🔑 KEY FUNCTIONS:
saveResume(content) — Upsert pattern for single-user resume
improveWithAI({ current, type }) — Gemini-powered resume enhancement
db.resume.upsert() — Atomic create-or-update operation
revalidatePath("/resume") — Cache busting after save
Purpose: Full CRUD for AI-generated cover letters using user profile + job description context.
FUNCTION: generateCoverLetter(data)
INPUT: { jobTitle, companyName, jobDescription }
1. auth() → userId
2. db.user.findUnique() → get user profile
3. Build context-rich prompt with:
- User: industry, experience, skills, bio
- Job: title, company, description
- Rules: professional tone, 400 words max, markdown format
4. model.generateContent(prompt) → Markdown letter
5. db.coverLetter.create({
content, jobDescription, companyName,
jobTitle, status: "completed", userId })
6. RETURN saved cover letter
────────────────────────────────────────────────────
FUNCTION: getCoverLetters()
→ db.coverLetter.findMany(userId, orderBy: desc)
FUNCTION: getCoverLetter(id)
→ db.coverLetter.findUnique({ id, userId })
FUNCTION: deleteCoverLetter(id)
→ db.coverLetter.delete({ id, userId })
🔑 KEY FUNCTIONS:
generateCoverLetter(data) — Context-rich Gemini letter generation
deleteCoverLetter(id) — Scoped deletion (user owns resource check)
getCoverLetters() — List with descending date sort
Purpose: Reusable React hook that wraps any async Server Action with loading, error, and data states. Eliminates async state boilerplate in every component.
HOOK: useFetch(cb)
INPUT: cb = any async Server Action function
STATE:
data = undefined (response data)
loading = null (boolean flag)
error = null (Error object)
FUNCTION fn(...args):
1. setLoading(true), setError(null)
2. TRY:
response = await cb(...args)
setData(response)
3. CATCH(error):
setError(error)
toast.error(error.message) ← automatic toast notification!
4. FINALLY: setLoading(false)
RETURN: { data, loading, error, fn, setData }
🔑 KEY PATTERN:
useFetch(generateQuiz) — wraps Server Action in React state
useFetch(saveQuizResult) — second instance for save operation
setData() — allows resetting state (e.g., startNewQuiz)
Purpose: Animated landing page with a typewriter effect, parallax scrolling image, and animated stats.
COMPONENT: HeroSection()
STATE:
index = current word index (0-4)
subIndex = current character position
isDeleting = typing direction flag
PARALLAX EFFECT:
scrollY = useScroll()
yParallax = useTransform(scrollY, [0, 600], [0, -40])
→ Applied to the right-side image panel
TYPING ENGINE (useEffect on [subIndex, isDeleting, index]):
IF subIndex === word.length+1 AND !isDeleting:
→ Wait 1500ms → setIsDeleting(true)
IF subIndex === 0 AND isDeleting:
→ setIsDeleting(false) → advance to next word
ELSE:
→ setTimeout(±1 char, 40ms delete / 80ms type)
RENDER:
• Animated h1 with gradient-title class (Framer Motion fade-in)
• Typewriter span + blinking cursor animation
• CTA buttons → /dashboard
• Stats row: [10k+ Engineers, 98% Placement, Instant Feedback]
• Parallax image panel (right side, float on scroll)
🔑 KEY FUNCTIONS:
useScroll() — Framer Motion scroll position tracker
useTransform(y, in, out) — Maps scroll position to CSS transform
useEffect([subIndex, ...]) — Drives the typing/deleting animation loop
motion.div / motion.span — Animated Framer Motion wrapper elements
Purpose: Client-side quiz engine that renders questions, captures answers with immediate visual feedback, calculates scores, and saves results.
COMPONENT: Quiz()
STATE:
currentQuestion = 0 (active question index)
answers[] = [] (user answer per question)
showExplanation = false (explanation toggle)
isCorrect = null (correctness feedback — null/true/false)
HOOKS:
useFetch(generateQuiz) → { loading: generatingQuiz, fn, data: quizData }
useFetch(saveQuizResult) → { loading: savingResult, fn, data: resultData }
────────────────────────────────────────────────────
FUNCTION: handleAnswer(answer)
1. Update answers[] at currentQuestion index
2. Compare answer vs quizData[i].correctAnswer
3. IF correct → setIsCorrect(true) + toast.success("Correct Answer!")
4. IF wrong → setIsCorrect(false) + toast.error("Wrong Answer!")
FUNCTION: handleNext()
1. IF more questions → advance currentQuestion
Reset showExplanation and isCorrect
2. IF last question → call finishQuiz()
FUNCTION: calculateScore()
→ Count correct answers in answers[]
→ RETURN (correct / total) * 100
FUNCTION: finishQuiz()
→ calculateScore()
→ saveQuizResultFn(quizData, answers, score)
FUNCTION: startNewQuiz()
→ Reset all state to initial values
→ generateQuizFn() → fetch fresh AI questions
RENDER STATES:
Loading → <BarLoader />
No data → "Start Quiz" card
Complete → <QuizResult result={...} onStartNew={startNewQuiz} />
Active → Question card with RadioGroup
• Green highlight on correct answer
• Red highlight on wrong answer
• "Show Explanation" toggle button
🔑 KEY FUNCTIONS:
handleAnswer(answer) — Instant answer validation + visual feedback
calculateScore() — Percentage score calculator
finishQuiz() — Score computation + server action trigger
startNewQuiz() — Full quiz state reset + new quiz fetch
useFetch(generateQuiz) — Async quiz generation with loading state
erDiagram
User ||--o{ Assessment : "has many"
User ||--o| Resume : "has one"
User ||--o{ CoverLetter : "has many"
User }o--|| IndustryInsight : "belongs to (by industry)"
User {
uuid id PK
string clerkUserId UK
string email UK
string name
string imageUrl
string industry FK
string bio
int experience
string_array skills
}
Assessment {
cuid id PK
uuid userId FK
float quizScore
json questions
string category
string improvementTip
datetime createdAt
}
Resume {
cuid id PK
uuid userId FK "unique"
text content
int atsScore
text feedback
}
CoverLetter {
cuid id PK
uuid userId FK
text content "Markdown"
text jobDescription
string companyName
string jobTitle
string status
}
IndustryInsight {
cuid id PK
string industry UK
json_array salaryRanges
float growthRate
string demandLevel
string_array topSkills
string marketOutlook
string_array keyTrends
string_array recommendedSkills
datetime lastUpdated
datetime nextUpdate
}
Create a .env file in the project root:
# ─── DATABASE ────────────────────────────────────────────
DATABASE_URL="postgresql://user:password@host:5432/dbname"
# ─── CLERK AUTH ──────────────────────────────────────────
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
# ─── GOOGLE GEMINI AI ────────────────────────────────────
GEMINI_API_KEY=AIza...
# ─── INNGEST ─────────────────────────────────────────────
INNGEST_EVENT_KEY=...
INNGEST_SIGNING_KEY=...- Node.js >= 18
- PostgreSQL database (local or Neon)
- Clerk account
- Google AI Studio API key
- Inngest account (free tier works)
# 1. Clone the repository
git clone https://github.com/your-username/ai-career-coach.git
cd ai-career-coach
# 2. Install dependencies
npm install
# 3. Set up environment variables
cp .env.example .env
# Edit .env with your credentials
# 4. Run database migrations
npx prisma migrate dev
# 5. Start the development server
npm run devApp runs at http://localhost:3000
To run Inngest locally (for background job testing):
npx inngest-cli@latest devnpm i -g vercel
vercel --prodSet all environment variables in your Vercel project dashboard under Settings → Environment Variables.
- Create a project at neon.tech
- Copy the connection string into
DATABASE_URL - Run
npx prisma migrate deploy
Register your production webhook at:
https://your-app.vercel.app/api/inngest
- Fork the repository
- Create a feature branch:
git checkout -b feat/your-feature - Commit:
git commit -m 'feat: add your feature' - Push:
git push origin feat/your-feature - Open a Pull Request
This project is licensed under the MIT License.
Built with ❤️ using Next.js · Gemini AI · Prisma · Clerk · Inngest
