├── app
├── favicon.ico
├── (auth)
│ ├── sign-in
│ │ └── [[...sign-in]]
│ │ │ └── page.jsx
│ ├── sign-up
│ │ └── [[...sign-up]]
│ │ │ └── page.jsx
│ └── layout.js
├── (main)
│ ├── layout.jsx
│ ├── resume
│ │ ├── page.jsx
│ │ └── _components
│ │ │ ├── entry-form.jsx
│ │ │ └── resume-builder.jsx
│ ├── interview
│ │ ├── layout.js
│ │ ├── page.jsx
│ │ ├── mock
│ │ │ └── page.jsx
│ │ └── _components
│ │ │ ├── performace-chart.jsx
│ │ │ ├── stats-cards.jsx
│ │ │ ├── quiz-result.jsx
│ │ │ ├── quiz-list.jsx
│ │ │ └── quiz.jsx
│ ├── ai-cover-letter
│ │ ├── _components
│ │ │ ├── cover-letter-preview.jsx
│ │ │ ├── cover-letter-list.jsx
│ │ │ └── cover-letter-generator.jsx
│ │ ├── page.jsx
│ │ ├── new
│ │ │ └── page.jsx
│ │ └── [id]
│ │ │ └── page.jsx
│ ├── dashboard
│ │ ├── layout.js
│ │ ├── page.jsx
│ │ └── _component
│ │ │ └── dashboard-view.jsx
│ └── onboarding
│ │ ├── page.jsx
│ │ └── _components
│ │ └── onboarding-form.jsx
├── api
│ └── inngest
│ │ └── route.js
├── lib
│ ├── helper.js
│ └── schema.js
├── not-found.jsx
├── layout.js
├── globals.css
└── page.js
├── public
├── logo.png
├── banner.jpeg
├── banner2.jpeg
└── banner3.jpeg
├── jsconfig.json
├── .eslintrc.json
├── lib
├── utils.js
├── inngest
│ ├── client.js
│ └── function.js
├── prisma.js
└── checkUser.js
├── prisma
├── migrations
│ ├── migration_lock.toml
│ ├── 20250117091806_update
│ │ └── migration.sql
│ ├── 20250114064152_update_user
│ │ └── migration.sql
│ ├── 20250120124732_update
│ │ └── migration.sql
│ ├── 20250120090020_hh
│ │ └── migration.sql
│ └── 20250114060115_create_models
│ │ └── migration.sql
└── schema.prisma
├── postcss.config.mjs
├── next.config.mjs
├── components
├── theme-provider.jsx
├── ui
│ ├── label.jsx
│ ├── textarea.jsx
│ ├── input.jsx
│ ├── progress.jsx
│ ├── sonner.jsx
│ ├── badge.jsx
│ ├── radio-group.jsx
│ ├── card.jsx
│ ├── tabs.jsx
│ ├── accordion.jsx
│ ├── button.jsx
│ ├── dialog.jsx
│ ├── alert-dialog.jsx
│ ├── select.jsx
│ └── dropdown-menu.jsx
├── hero.jsx
└── header.jsx
├── eslint.config.mjs
├── components.json
├── README.md
├── .gitignore
├── hooks
└── use-fetch.js
├── data
├── howItWorks.js
├── testimonial.js
├── features.js
├── faqs.js
└── industries.js
├── middleware.js
├── package.json
├── tailwind.config.mjs
└── actions
├── dashboard.js
├── user.js
├── resume.js
├── cover-letter.js
└── interview.js
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush-eon/ai-career-coach/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush-eon/ai-career-coach/HEAD/public/logo.png
--------------------------------------------------------------------------------
/public/banner.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush-eon/ai-career-coach/HEAD/public/banner.jpeg
--------------------------------------------------------------------------------
/public/banner2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush-eon/ai-career-coach/HEAD/public/banner2.jpeg
--------------------------------------------------------------------------------
/public/banner3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piyush-eon/ai-career-coach/HEAD/public/banner3.jpeg
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "no-unused-vars": ["warn"]
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/app/(auth)/sign-in/[[...sign-in]]/page.jsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/app/(auth)/sign-up/[[...sign-up]]/page.jsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (e.g., Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/app/(auth)/layout.js:
--------------------------------------------------------------------------------
1 | const AuthLayout = ({ children }) => {
2 | return
{children}
;
3 | };
4 |
5 | export default AuthLayout;
6 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/app/(main)/layout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const MainLayout = async ({ children }) => {
4 | return {children}
;
5 | };
6 |
7 | export default MainLayout;
8 |
--------------------------------------------------------------------------------
/prisma/migrations/20250117091806_update/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `title` on the `User` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "User" DROP COLUMN "title";
9 |
--------------------------------------------------------------------------------
/lib/inngest/client.js:
--------------------------------------------------------------------------------
1 | import { Inngest } from "inngest";
2 |
3 | export const inngest = new Inngest({
4 | id: "career-coach", // Unique app ID
5 | name: "Career Coach",
6 | credentials: {
7 | gemini: {
8 | apiKey: process.env.GEMINI_API_KEY,
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "https",
7 | hostname: "randomuser.me",
8 | },
9 | ],
10 | },
11 | };
12 |
13 | export default nextConfig;
14 |
--------------------------------------------------------------------------------
/components/theme-provider.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 |
6 | export function ThemeProvider({ children, ...props }) {
7 | return {children} ;
8 | }
9 |
--------------------------------------------------------------------------------
/app/api/inngest/route.js:
--------------------------------------------------------------------------------
1 | import { serve } from "inngest/next";
2 |
3 | import { inngest } from "@/lib/inngest/client";
4 | import { generateIndustryInsights } from "@/lib/inngest/function";
5 |
6 | export const { GET, POST, PUT } = serve({
7 | client: inngest,
8 | functions: [generateIndustryInsights],
9 | });
10 |
--------------------------------------------------------------------------------
/app/(main)/resume/page.jsx:
--------------------------------------------------------------------------------
1 | import { getResume } from "@/actions/resume";
2 | import ResumeBuilder from "./_components/resume-builder";
3 |
4 | export default async function ResumePage() {
5 | const resume = await getResume();
6 |
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/prisma/migrations/20250114064152_update_user/migration.sql:
--------------------------------------------------------------------------------
1 | -- DropForeignKey
2 | ALTER TABLE "User" DROP CONSTRAINT "User_industry_fkey";
3 |
4 | -- AlterTable
5 | ALTER TABLE "User" ALTER COLUMN "industry" DROP NOT NULL;
6 |
7 | -- AddForeignKey
8 | ALTER TABLE "User" ADD CONSTRAINT "User_industry_fkey" FOREIGN KEY ("industry") REFERENCES "IndustryInsight"("industry") ON DELETE SET NULL ON UPDATE CASCADE;
9 |
--------------------------------------------------------------------------------
/app/(main)/interview/layout.js:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { BarLoader } from "react-spinners";
3 |
4 | export default function Layout({ children }) {
5 | return (
6 |
7 | }
9 | >
10 | {children}
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/app/(main)/ai-cover-letter/_components/cover-letter-preview.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import MDEditor from "@uiw/react-md-editor";
5 |
6 | const CoverLetterPreview = ({ content }) => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default CoverLetterPreview;
15 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [...compat.extends("next/core-web-vitals")];
13 |
14 | export default eslintConfig;
15 |
--------------------------------------------------------------------------------
/lib/prisma.js:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | export const db = globalThis.prisma || new PrismaClient();
4 |
5 | if (process.env.NODE_ENV !== "production") {
6 | globalThis.prisma = db;
7 | }
8 |
9 | // globalThis.prisma: This global variable ensures that the Prisma client instance is
10 | // reused across hot reloads during development. Without this, each time your application
11 | // reloads, a new instance of the Prisma client would be created, potentially leading
12 | // to connection issues.
13 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": false,
6 | "tailwind": {
7 | "config": "tailwind.config.mjs",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/app/lib/helper.js:
--------------------------------------------------------------------------------
1 | // Helper function to convert entries to markdown
2 | export function entriesToMarkdown(entries, type) {
3 | if (!entries?.length) return "";
4 |
5 | return (
6 | `## ${type}\n\n` +
7 | entries
8 | .map((entry) => {
9 | const dateRange = entry.current
10 | ? `${entry.startDate} - Present`
11 | : `${entry.startDate} - ${entry.endDate}`;
12 | return `### ${entry.title} @ ${entry.organization}\n${dateRange}\n\n${entry.description}`;
13 | })
14 | .join("\n\n")
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/app/(main)/dashboard/layout.js:
--------------------------------------------------------------------------------
1 | import { BarLoader } from "react-spinners";
2 | import { Suspense } from "react";
3 |
4 | export default function Layout({ children }) {
5 | return (
6 |
7 |
8 |
Industry Insights
9 |
10 |
}
12 | >
13 | {children}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/app/(main)/onboarding/page.jsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import { industries } from "@/data/industries";
3 | import OnboardingForm from "./_components/onboarding-form";
4 | import { getUserOnboardingStatus } from "@/actions/user";
5 |
6 | export default async function OnboardingPage() {
7 | // Check if user is already onboarded
8 | const { isOnboarded } = await getUserOnboardingStatus();
9 |
10 | if (isOnboarded) {
11 | redirect("/dashboard");
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/ui/label.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef(({ className, ...props }, ref) => (
14 |
15 | ))
16 | Label.displayName = LabelPrimitive.Root.displayName
17 |
18 | export { Label }
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Full Stack AI Career Coach with Next JS, Neon DB, Tailwind, Prisma, Inngest, Shadcn UI Tutorial 🔥🔥
2 | ## https://youtu.be/UbXpRv5ApKA
3 |
4 | 
5 |
6 | ### Make sure to create a `.env` file with following variables -
7 |
8 | ```
9 | DATABASE_URL=
10 |
11 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
12 | CLERK_SECRET_KEY=
13 |
14 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
15 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
16 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/onboarding
17 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
18 |
19 | GEMINI_API_KEY=
20 | ```
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/app/not-found.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Button } from "@/components/ui/button";
3 |
4 | export default function NotFound() {
5 | return (
6 |
7 |
404
8 |
Page Not Found
9 |
10 | Oops! The page you're looking for doesn't exist or has been
11 | moved.
12 |
13 |
14 |
Return Home
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/ui/textarea.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
6 | return (
7 | ()
14 | );
15 | })
16 | Textarea.displayName = "Textarea"
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/prisma/migrations/20250120124732_update/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `companyName` to the `CoverLetter` table without a default value. This is not possible if the table is not empty.
5 | - Added the required column `jobTitle` to the `CoverLetter` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- DropIndex
9 | DROP INDEX "CoverLetter_userId_key";
10 |
11 | -- AlterTable
12 | ALTER TABLE "CoverLetter" ADD COLUMN "companyName" TEXT NOT NULL,
13 | ADD COLUMN "jobTitle" TEXT NOT NULL,
14 | ADD COLUMN "status" TEXT NOT NULL DEFAULT 'draft';
15 |
16 | -- CreateIndex
17 | CREATE INDEX "CoverLetter_userId_idx" ON "CoverLetter"("userId");
18 |
--------------------------------------------------------------------------------
/hooks/use-fetch.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { toast } from "sonner";
3 |
4 | const useFetch = (cb) => {
5 | const [data, setData] = useState(undefined);
6 | const [loading, setLoading] = useState(null);
7 | const [error, setError] = useState(null);
8 |
9 | const fn = async (...args) => {
10 | setLoading(true);
11 | setError(null);
12 |
13 | try {
14 | const response = await cb(...args);
15 | setData(response);
16 | setError(null);
17 | } catch (error) {
18 | setError(error);
19 | toast.error(error.message);
20 | } finally {
21 | setLoading(false);
22 | }
23 | };
24 |
25 | return { data, loading, error, fn, setData };
26 | };
27 |
28 | export default useFetch;
29 |
--------------------------------------------------------------------------------
/app/(main)/dashboard/page.jsx:
--------------------------------------------------------------------------------
1 | import { getIndustryInsights } from "@/actions/dashboard";
2 | import DashboardView from "./_component/dashboard-view";
3 | import { getUserOnboardingStatus } from "@/actions/user";
4 | import { redirect } from "next/navigation";
5 |
6 | export default async function DashboardPage() {
7 | const { isOnboarded } = await getUserOnboardingStatus();
8 |
9 | // If not onboarded, redirect to onboarding page
10 | // Skip this check if already on the onboarding page
11 | if (!isOnboarded) {
12 | redirect("/onboarding");
13 | }
14 |
15 | const insights = await getIndustryInsights();
16 |
17 | return (
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/ui/input.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
6 | return (
7 | ( )
15 | );
16 | })
17 | Input.displayName = "Input"
18 |
19 | export { Input }
20 |
--------------------------------------------------------------------------------
/components/ui/progress.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
9 |
16 |
19 |
20 | ))
21 | Progress.displayName = ProgressPrimitive.Root.displayName
22 |
23 | export { Progress }
24 |
--------------------------------------------------------------------------------
/app/(main)/interview/page.jsx:
--------------------------------------------------------------------------------
1 | import { getAssessments } from "@/actions/interview";
2 | import StatsCards from "./_components/stats-cards";
3 | import PerformanceChart from "./_components/performace-chart";
4 | import QuizList from "./_components/quiz-list";
5 |
6 | export default async function InterviewPrepPage() {
7 | const assessments = await getAssessments();
8 |
9 | return (
10 |
11 |
12 |
13 | Interview Preparation
14 |
15 |
16 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/lib/checkUser.js:
--------------------------------------------------------------------------------
1 | import { currentUser } from "@clerk/nextjs/server";
2 | import { db } from "./prisma";
3 |
4 | export const checkUser = async () => {
5 | const user = await currentUser();
6 |
7 | if (!user) {
8 | return null;
9 | }
10 |
11 | try {
12 | const loggedInUser = await db.user.findUnique({
13 | where: {
14 | clerkUserId: user.id,
15 | },
16 | });
17 |
18 | if (loggedInUser) {
19 | return loggedInUser;
20 | }
21 |
22 | const name = `${user.firstName} ${user.lastName}`;
23 |
24 | const newUser = await db.user.create({
25 | data: {
26 | clerkUserId: user.id,
27 | name,
28 | imageUrl: user.imageUrl,
29 | email: user.emailAddresses[0].emailAddress,
30 | },
31 | });
32 |
33 | return newUser;
34 | } catch (error) {
35 | console.log(error.message);
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/app/(main)/ai-cover-letter/page.jsx:
--------------------------------------------------------------------------------
1 | import { getCoverLetters } from "@/actions/cover-letter";
2 | import Link from "next/link";
3 | import { Plus } from "lucide-react";
4 | import { Button } from "@/components/ui/button";
5 | import CoverLetterList from "./_components/cover-letter-list";
6 |
7 | export default async function CoverLetterPage() {
8 | const coverLetters = await getCoverLetters();
9 |
10 | return (
11 |
12 |
13 |
My Cover Letters
14 |
15 |
16 |
17 | Create New
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/components/ui/sonner.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useTheme } from "next-themes"
3 | import { Toaster as Sonner } from "sonner"
4 |
5 | const Toaster = ({
6 | ...props
7 | }) => {
8 | const { theme = "system" } = useTheme()
9 |
10 | return (
11 | ( )
26 | );
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/data/howItWorks.js:
--------------------------------------------------------------------------------
1 | import { UserPlus, FileEdit, Users, LineChart } from "lucide-react";
2 |
3 | export const howItWorks = [
4 | {
5 | title: "Professional Onboarding",
6 | description: "Share your industry and expertise for personalized guidance",
7 | icon: ,
8 | },
9 | {
10 | title: "Craft Your Documents",
11 | description: "Create ATS-optimized resumes and compelling cover letters",
12 | icon: ,
13 | },
14 | {
15 | title: "Prepare for Interviews",
16 | description:
17 | "Practice with AI-powered mock interviews tailored to your role",
18 | icon: ,
19 | },
20 | {
21 | title: "Track Your Progress",
22 | description: "Monitor improvements with detailed performance analytics",
23 | icon: ,
24 | },
25 | ];
26 |
--------------------------------------------------------------------------------
/middleware.js:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2 | import { NextResponse } from "next/server";
3 |
4 | const isProtectedRoute = createRouteMatcher([
5 | "/dashboard(.*)",
6 | "/resume(.*)",
7 | "/interview(.*)",
8 | "/ai-cover-letter(.*)",
9 | "/onboarding(.*)",
10 | ]);
11 |
12 | export default clerkMiddleware(async (auth, req) => {
13 | const { userId } = await auth();
14 |
15 | if (!userId && isProtectedRoute(req)) {
16 | const { redirectToSignIn } = await auth();
17 | return redirectToSignIn();
18 | }
19 |
20 | return NextResponse.next();
21 | });
22 |
23 | export const config = {
24 | matcher: [
25 | // Skip Next.js internals and all static files, unless found in search params
26 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
27 | // Always run for API routes
28 | "/(api|trpc)(.*)",
29 | ],
30 | };
31 |
--------------------------------------------------------------------------------
/app/(main)/interview/mock/page.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { ArrowLeft } from "lucide-react";
3 | import { Button } from "@/components/ui/button";
4 | import Quiz from "../_components/quiz";
5 |
6 | export default function MockInterviewPage() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | Back to Interview Preparation
14 |
15 |
16 |
17 |
18 |
Mock Interview
19 |
20 | Test your knowledge with industry-specific questions
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/data/testimonial.js:
--------------------------------------------------------------------------------
1 | export const testimonial = [
2 | {
3 | quote:
4 | "The AI-powered interview prep was a game-changer. Landed my dream job at a top tech company!",
5 | author: "Sarah Chen",
6 | image: "https://randomuser.me/api/portraits/women/75.jpg",
7 | role: "Software Engineer",
8 | company: "Tech Giant Co.",
9 | },
10 | {
11 | quote:
12 | "The industry insights helped me pivot my career successfully. The salary data was spot-on!",
13 | author: "Michael Rodriguez",
14 | image: "https://randomuser.me/api/portraits/men/75.jpg",
15 | role: "Product Manager",
16 | company: "StartUp Inc.",
17 | },
18 | {
19 | quote:
20 | "My resume's ATS score improved significantly. Got more interviews in two weeks than in six months!",
21 | author: "Priya Patel",
22 | image: "https://randomuser.me/api/portraits/women/74.jpg",
23 | role: "Marketing Director",
24 | company: "Global Corp",
25 | },
26 | ];
27 |
--------------------------------------------------------------------------------
/prisma/migrations/20250120090020_hh/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `answer` on the `Assessment` table. All the data in the column will be lost.
5 | - You are about to drop the column `question` on the `Assessment` table. All the data in the column will be lost.
6 | - You are about to drop the column `score` on the `Assessment` table. All the data in the column will be lost.
7 | - You are about to drop the column `userAnswer` on the `Assessment` table. All the data in the column will be lost.
8 | - Added the required column `quizScore` to the `Assessment` table without a default value. This is not possible if the table is not empty.
9 |
10 | */
11 | -- AlterTable
12 | ALTER TABLE "Assessment" DROP COLUMN "answer",
13 | DROP COLUMN "question",
14 | DROP COLUMN "score",
15 | DROP COLUMN "userAnswer",
16 | ADD COLUMN "improvementTip" TEXT,
17 | ADD COLUMN "questions" JSONB[],
18 | ADD COLUMN "quizScore" DOUBLE PRECISION NOT NULL;
19 |
--------------------------------------------------------------------------------
/app/(main)/ai-cover-letter/new/page.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { ArrowLeft } from "lucide-react";
3 | import { Button } from "@/components/ui/button";
4 | import CoverLetterGenerator from "../_components/cover-letter-generator";
5 |
6 | export default function NewCoverLetterPage() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | Back to Cover Letters
14 |
15 |
16 |
17 |
18 |
19 | Create Cover Letter
20 |
21 |
22 | Generate a tailored cover letter for your job application
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/data/features.js:
--------------------------------------------------------------------------------
1 | import { BrainCircuit, Briefcase, LineChart, ScrollText } from "lucide-react";
2 |
3 | export const features = [
4 | {
5 | icon: ,
6 | title: "AI-Powered Career Guidance",
7 | description:
8 | "Get personalized career advice and insights powered by advanced AI technology.",
9 | },
10 | {
11 | icon: ,
12 | title: "Interview Preparation",
13 | description:
14 | "Practice with role-specific questions and get instant feedback to improve your performance.",
15 | },
16 | {
17 | icon: ,
18 | title: "Industry Insights",
19 | description:
20 | "Stay ahead with real-time industry trends, salary data, and market analysis.",
21 | },
22 | {
23 | icon: ,
24 | title: "Smart Resume Creation",
25 | description: "Generate ATS-optimized resumes with AI assistance.",
26 | },
27 | ];
28 |
--------------------------------------------------------------------------------
/app/(main)/ai-cover-letter/[id]/page.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { ArrowLeft } from "lucide-react";
3 | import { Button } from "@/components/ui/button";
4 | import { getCoverLetter } from "@/actions/cover-letter";
5 | import CoverLetterPreview from "../_components/cover-letter-preview";
6 |
7 | export default async function EditCoverLetterPage({ params }) {
8 | const { id } = await params;
9 | const coverLetter = await getCoverLetter(id);
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | Back to Cover Letters
18 |
19 |
20 |
21 |
22 | {coverLetter?.jobTitle} at {coverLetter?.companyName}
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/components/ui/badge.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | function Badge({
27 | className,
28 | variant,
29 | ...props
30 | }) {
31 | return (
);
32 | }
33 |
34 | export { Badge, badgeVariants }
35 |
--------------------------------------------------------------------------------
/components/ui/radio-group.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef(({ className, ...props }, ref) => {
10 | return ( );
11 | })
12 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
13 |
14 | const RadioGroupItem = React.forwardRef(({ className, ...props }, ref) => {
15 | return (
16 | (
23 |
24 |
25 |
26 | )
27 | );
28 | })
29 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
30 |
31 | export { RadioGroup, RadioGroupItem }
32 |
--------------------------------------------------------------------------------
/app/layout.js:
--------------------------------------------------------------------------------
1 | import { Inter } from "next/font/google";
2 | import "./globals.css";
3 | import { ClerkProvider } from "@clerk/nextjs";
4 | import { Toaster } from "sonner";
5 | import Header from "@/components/header";
6 | import { ThemeProvider } from "@/components/theme-provider";
7 | import { dark } from "@clerk/themes";
8 |
9 | const inter = Inter({ subsets: ["latin"] });
10 |
11 | export const metadata = {
12 | title: "AI Career Coach",
13 | description: "",
14 | };
15 |
16 | export default function RootLayout({ children }) {
17 | return (
18 |
23 |
24 |
25 |
26 |
27 |
28 |
34 |
35 | {children}
36 |
37 |
38 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/ui/card.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef(({ className, ...props }, ref) => (
6 |
10 | ))
11 | Card.displayName = "Card"
12 |
13 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
14 |
18 | ))
19 | CardHeader.displayName = "CardHeader"
20 |
21 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
22 |
26 | ))
27 | CardTitle.displayName = "CardTitle"
28 |
29 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
30 |
34 | ))
35 | CardDescription.displayName = "CardDescription"
36 |
37 | const CardContent = React.forwardRef(({ className, ...props }, ref) => (
38 |
39 | ))
40 | CardContent.displayName = "CardContent"
41 |
42 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
43 |
47 | ))
48 | CardFooter.displayName = "CardFooter"
49 |
50 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
51 |
--------------------------------------------------------------------------------
/components/ui/tabs.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef(({ className, ...props }, ref) => (
11 |
18 | ))
19 | TabsList.displayName = TabsPrimitive.List.displayName
20 |
21 | const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
22 |
29 | ))
30 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
31 |
32 | const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
33 |
40 | ))
41 | TabsContent.displayName = TabsPrimitive.Content.displayName
42 |
43 | export { Tabs, TabsList, TabsTrigger, TabsContent }
44 |
--------------------------------------------------------------------------------
/components/ui/accordion.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
12 |
13 | ))
14 | AccordionItem.displayName = "AccordionItem"
15 |
16 | const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
17 |
18 | svg]:rotate-180",
22 | className
23 | )}
24 | {...props}>
25 | {children}
26 |
28 |
29 |
30 | ))
31 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
32 |
33 | const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
34 |
38 | {children}
39 |
40 | ))
41 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
42 |
43 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ai-career-coach",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^6.9.10",
14 | "@clerk/themes": "^2.2.5",
15 | "@google/generative-ai": "^0.21.0",
16 | "@hookform/resolvers": "^3.10.0",
17 | "@prisma/client": "^6.2.1",
18 | "@radix-ui/react-accordion": "^1.2.2",
19 | "@radix-ui/react-alert-dialog": "^1.1.4",
20 | "@radix-ui/react-dialog": "^1.1.4",
21 | "@radix-ui/react-dropdown-menu": "^2.1.4",
22 | "@radix-ui/react-label": "^2.1.1",
23 | "@radix-ui/react-progress": "^1.1.1",
24 | "@radix-ui/react-radio-group": "^1.2.2",
25 | "@radix-ui/react-select": "^2.1.4",
26 | "@radix-ui/react-slot": "^1.1.1",
27 | "@radix-ui/react-tabs": "^1.1.2",
28 | "@uiw/react-md-editor": "^4.0.5",
29 | "class-variance-authority": "^0.7.1",
30 | "clsx": "^2.1.1",
31 | "date-fns": "^4.1.0",
32 | "html2pdf.js": "^0.10.2",
33 | "inngest": "^3.29.3",
34 | "lucide-react": "^0.471.1",
35 | "next": "15.1.4",
36 | "next-themes": "^0.4.4",
37 | "react": "^19.0.0",
38 | "react-dom": "^19.0.0",
39 | "react-hook-form": "^7.54.2",
40 | "react-markdown": "^9.0.3",
41 | "react-spinners": "^0.15.0",
42 | "recharts": "^2.15.0",
43 | "sonner": "^1.7.1",
44 | "tailwind-merge": "^2.6.0",
45 | "tailwindcss-animate": "^1.0.7",
46 | "zod": "^3.24.1"
47 | },
48 | "devDependencies": {
49 | "@eslint/eslintrc": "^3",
50 | "eslint": "^9",
51 | "eslint-config-next": "15.1.4",
52 | "postcss": "^8",
53 | "prisma": "^6.2.1",
54 | "tailwindcss": "^3.4.1"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/components/ui/button.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
38 | const Comp = asChild ? Slot : "button"
39 | return (
40 | ( )
44 | );
45 | })
46 | Button.displayName = "Button"
47 |
48 | export { Button, buttonVariants }
49 |
--------------------------------------------------------------------------------
/app/lib/schema.js:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const onboardingSchema = z.object({
4 | industry: z.string({
5 | required_error: "Please select an industry",
6 | }),
7 | subIndustry: z.string({
8 | required_error: "Please select a specialization",
9 | }),
10 | bio: z.string().max(500).optional(),
11 | experience: z
12 | .string()
13 | .transform((val) => parseInt(val, 10))
14 | .pipe(
15 | z
16 | .number()
17 | .min(0, "Experience must be at least 0 years")
18 | .max(50, "Experience cannot exceed 50 years")
19 | ),
20 | skills: z.string().transform((val) =>
21 | val
22 | ? val
23 | .split(",")
24 | .map((skill) => skill.trim())
25 | .filter(Boolean)
26 | : undefined
27 | ),
28 | });
29 |
30 | export const contactSchema = z.object({
31 | email: z.string().email("Invalid email address"),
32 | mobile: z.string().optional(),
33 | linkedin: z.string().optional(),
34 | twitter: z.string().optional(),
35 | });
36 |
37 | export const entrySchema = z
38 | .object({
39 | title: z.string().min(1, "Title is required"),
40 | organization: z.string().min(1, "Organization is required"),
41 | startDate: z.string().min(1, "Start date is required"),
42 | endDate: z.string().optional(),
43 | description: z.string().min(1, "Description is required"),
44 | current: z.boolean().default(false),
45 | })
46 | .refine(
47 | (data) => {
48 | if (!data.current && !data.endDate) {
49 | return false;
50 | }
51 | return true;
52 | },
53 | {
54 | message: "End date is required unless this is your current position",
55 | path: ["endDate"],
56 | }
57 | );
58 |
59 | export const resumeSchema = z.object({
60 | contactInfo: contactSchema,
61 | summary: z.string().min(1, "Professional summary is required"),
62 | skills: z.string().min(1, "Skills are required"),
63 | experience: z.array(entrySchema),
64 | education: z.array(entrySchema),
65 | projects: z.array(entrySchema),
66 | });
67 |
68 | export const coverLetterSchema = z.object({
69 | companyName: z.string().min(1, "Company name is required"),
70 | jobTitle: z.string().min(1, "Job title is required"),
71 | jobDescription: z.string().min(1, "Job description is required"),
72 | });
73 |
--------------------------------------------------------------------------------
/data/faqs.js:
--------------------------------------------------------------------------------
1 | export const faqs = [
2 | {
3 | question: "What makes Sensai unique as a career development tool?",
4 | answer:
5 | "Sensai combines AI-powered career tools with industry-specific insights to help you advance your career. Our platform offers three main features: an intelligent resume builder, a cover letter generator, and an adaptive interview preparation system. Each tool is tailored to your industry and skills, providing personalized guidance for your professional journey.",
6 | },
7 | {
8 | question: "How does Sensai create tailored content?",
9 | answer:
10 | "Sensai learns about your industry, experience, and skills during onboarding. It then uses this information to generate customized resumes, cover letters, and interview questions. The content is specifically aligned with your professional background and industry standards, making it highly relevant and effective.",
11 | },
12 | {
13 | question: "How accurate and up-to-date are Sensai's industry insights?",
14 | answer:
15 | "We update our industry insights weekly using advanced AI analysis of current market trends. This includes salary data, in-demand skills, and industry growth patterns. Our system constantly evolves to ensure you have the most relevant information for your career decisions.",
16 | },
17 | {
18 | question: "Is my data secure with Sensai?",
19 | answer:
20 | "Absolutely. We prioritize the security of your professional information. All data is encrypted and securely stored using industry-standard practices. We use Clerk for authentication and never share your personal information with third parties.",
21 | },
22 | {
23 | question: "How can I track my interview preparation progress?",
24 | answer:
25 | "Sensai tracks your performance across multiple practice interviews, providing detailed analytics and improvement suggestions. You can view your progress over time, identify areas for improvement, and receive AI-generated tips to enhance your interview skills based on your responses.",
26 | },
27 | {
28 | question: "Can I edit the AI-generated content?",
29 | answer:
30 | "Yes! While Sensai generates high-quality initial content, you have full control to edit and customize all generated resumes, cover letters, and other content. Our markdown editor makes it easy to refine the content to perfectly match your needs.",
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: 'hsl(var(--background))',
13 | foreground: 'hsl(var(--foreground))',
14 | card: {
15 | DEFAULT: 'hsl(var(--card))',
16 | foreground: 'hsl(var(--card-foreground))'
17 | },
18 | popover: {
19 | DEFAULT: 'hsl(var(--popover))',
20 | foreground: 'hsl(var(--popover-foreground))'
21 | },
22 | primary: {
23 | DEFAULT: 'hsl(var(--primary))',
24 | foreground: 'hsl(var(--primary-foreground))'
25 | },
26 | secondary: {
27 | DEFAULT: 'hsl(var(--secondary))',
28 | foreground: 'hsl(var(--secondary-foreground))'
29 | },
30 | muted: {
31 | DEFAULT: 'hsl(var(--muted))',
32 | foreground: 'hsl(var(--muted-foreground))'
33 | },
34 | accent: {
35 | DEFAULT: 'hsl(var(--accent))',
36 | foreground: 'hsl(var(--accent-foreground))'
37 | },
38 | destructive: {
39 | DEFAULT: 'hsl(var(--destructive))',
40 | foreground: 'hsl(var(--destructive-foreground))'
41 | },
42 | border: 'hsl(var(--border))',
43 | input: 'hsl(var(--input))',
44 | ring: 'hsl(var(--ring))',
45 | chart: {
46 | '1': 'hsl(var(--chart-1))',
47 | '2': 'hsl(var(--chart-2))',
48 | '3': 'hsl(var(--chart-3))',
49 | '4': 'hsl(var(--chart-4))',
50 | '5': 'hsl(var(--chart-5))'
51 | }
52 | },
53 | borderRadius: {
54 | lg: 'var(--radius)',
55 | md: 'calc(var(--radius) - 2px)',
56 | sm: 'calc(var(--radius) - 4px)'
57 | },
58 | keyframes: {
59 | 'accordion-down': {
60 | from: {
61 | height: '0'
62 | },
63 | to: {
64 | height: 'var(--radix-accordion-content-height)'
65 | }
66 | },
67 | 'accordion-up': {
68 | from: {
69 | height: 'var(--radix-accordion-content-height)'
70 | },
71 | to: {
72 | height: '0'
73 | }
74 | }
75 | },
76 | animation: {
77 | 'accordion-down': 'accordion-down 0.2s ease-out',
78 | 'accordion-up': 'accordion-up 0.2s ease-out'
79 | }
80 | }
81 | },
82 | plugins: [require("tailwindcss-animate")],
83 | };
84 |
--------------------------------------------------------------------------------
/components/hero.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useRef } from "react";
4 | import Image from "next/image";
5 | import { Button } from "@/components/ui/button";
6 | import Link from "next/link";
7 |
8 | const HeroSection = () => {
9 | const imageRef = useRef(null);
10 |
11 | useEffect(() => {
12 | const imageElement = imageRef.current;
13 |
14 | const handleScroll = () => {
15 | const scrollPosition = window.scrollY;
16 | const scrollThreshold = 100;
17 |
18 | if (scrollPosition > scrollThreshold) {
19 | imageElement.classList.add("scrolled");
20 | } else {
21 | imageElement.classList.remove("scrolled");
22 | }
23 | };
24 |
25 | window.addEventListener("scroll", handleScroll);
26 | return () => window.removeEventListener("scroll", handleScroll);
27 | }, []);
28 |
29 | return (
30 |
31 |
32 |
33 |
34 | Your AI Career Coach for
35 |
36 | Professional Success
37 |
38 |
39 | Advance your career with personalized guidance, interview prep, and
40 | AI-powered tools for job success.
41 |
42 |
43 |
44 |
45 |
46 | Get Started
47 |
48 |
49 |
50 |
51 | Watch Demo
52 |
53 |
54 |
55 |
67 |
68 |
69 | );
70 | };
71 |
72 | export default HeroSection;
73 |
--------------------------------------------------------------------------------
/actions/dashboard.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth } from "@clerk/nextjs/server";
5 | import { GoogleGenerativeAI } from "@google/generative-ai";
6 |
7 | const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
8 | const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
9 |
10 | export const generateAIInsights = async (industry) => {
11 | const prompt = `
12 | Analyze the current state of the ${industry} industry and provide insights in ONLY the following JSON format without any additional notes or explanations:
13 | {
14 | "salaryRanges": [
15 | { "role": "string", "min": number, "max": number, "median": number, "location": "string" }
16 | ],
17 | "growthRate": number,
18 | "demandLevel": "High" | "Medium" | "Low",
19 | "topSkills": ["skill1", "skill2"],
20 | "marketOutlook": "Positive" | "Neutral" | "Negative",
21 | "keyTrends": ["trend1", "trend2"],
22 | "recommendedSkills": ["skill1", "skill2"]
23 | }
24 |
25 | IMPORTANT: Return ONLY the JSON. No additional text, notes, or markdown formatting.
26 | Include at least 5 common roles for salary ranges.
27 | Growth rate should be a percentage.
28 | Include at least 5 skills and trends.
29 | `;
30 |
31 | const result = await model.generateContent(prompt);
32 | const response = result.response;
33 | const text = response.text();
34 | const cleanedText = text.replace(/```(?:json)?\n?/g, "").trim();
35 |
36 | return JSON.parse(cleanedText);
37 | };
38 |
39 | export async function getIndustryInsights() {
40 | const { userId } = await auth();
41 | if (!userId) throw new Error("Unauthorized");
42 |
43 | const user = await db.user.findUnique({
44 | where: { clerkUserId: userId },
45 | include: {
46 | industryInsight: true,
47 | },
48 | });
49 |
50 | if (!user) throw new Error("User not found");
51 |
52 | // If no insights exist, generate them
53 | if (!user.industryInsight) {
54 | const insights = await generateAIInsights(user.industry);
55 |
56 | const industryInsight = await db.industryInsight.create({
57 | data: {
58 | industry: user.industry,
59 | ...insights,
60 | nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
61 | },
62 | });
63 |
64 | return industryInsight;
65 | }
66 |
67 | return user.industryInsight;
68 | }
69 |
--------------------------------------------------------------------------------
/app/(main)/interview/_components/performace-chart.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | LineChart,
5 | Line,
6 | XAxis,
7 | YAxis,
8 | CartesianGrid,
9 | Tooltip,
10 | ResponsiveContainer,
11 | } from "recharts";
12 | import {
13 | Card,
14 | CardContent,
15 | CardDescription,
16 | CardHeader,
17 | CardTitle,
18 | } from "@/components/ui/card";
19 | import { useEffect, useState } from "react";
20 | import { format } from "date-fns";
21 |
22 | export default function PerformanceChart({ assessments }) {
23 | const [chartData, setChartData] = useState([]);
24 |
25 | useEffect(() => {
26 | if (assessments) {
27 | const formattedData = assessments.map((assessment) => ({
28 | date: format(new Date(assessment.createdAt), "MMM dd"),
29 | score: assessment.quizScore,
30 | }));
31 | setChartData(formattedData);
32 | }
33 | }, [assessments]);
34 |
35 | return (
36 |
37 |
38 |
39 | Performance Trend
40 |
41 | Your quiz scores over time
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {
52 | if (active && payload?.length) {
53 | return (
54 |
55 |
56 | Score: {payload[0].value}%
57 |
58 |
59 | {payload[0].payload.date}
60 |
61 |
62 | );
63 | }
64 | return null;
65 | }}
66 | />
67 |
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/lib/inngest/function.js:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/prisma";
2 | import { inngest } from "./client";
3 | import { GoogleGenerativeAI } from "@google/generative-ai";
4 |
5 | const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
6 | const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
7 |
8 | export const generateIndustryInsights = inngest.createFunction(
9 | { name: "Generate Industry Insights" },
10 | { cron: "0 0 * * 0" }, // Run every Sunday at midnight
11 | async ({ event, step }) => {
12 | const industries = await step.run("Fetch industries", async () => {
13 | return await db.industryInsight.findMany({
14 | select: { industry: true },
15 | });
16 | });
17 |
18 | for (const { industry } of industries) {
19 | const prompt = `
20 | Analyze the current state of the ${industry} industry and provide insights in ONLY the following JSON format without any additional notes or explanations:
21 | {
22 | "salaryRanges": [
23 | { "role": "string", "min": number, "max": number, "median": number, "location": "string" }
24 | ],
25 | "growthRate": number,
26 | "demandLevel": "High" | "Medium" | "Low",
27 | "topSkills": ["skill1", "skill2"],
28 | "marketOutlook": "Positive" | "Neutral" | "Negative",
29 | "keyTrends": ["trend1", "trend2"],
30 | "recommendedSkills": ["skill1", "skill2"]
31 | }
32 |
33 | IMPORTANT: Return ONLY the JSON. No additional text, notes, or markdown formatting.
34 | Include at least 5 common roles for salary ranges.
35 | Growth rate should be a percentage.
36 | Include at least 5 skills and trends.
37 | `;
38 |
39 | const res = await step.ai.wrap(
40 | "gemini",
41 | async (p) => {
42 | return await model.generateContent(p);
43 | },
44 | prompt
45 | );
46 |
47 | const text = res.response.candidates[0].content.parts[0].text || "";
48 | const cleanedText = text.replace(/```(?:json)?\n?/g, "").trim();
49 |
50 | const insights = JSON.parse(cleanedText);
51 |
52 | await step.run(`Update ${industry} insights`, async () => {
53 | await db.industryInsight.update({
54 | where: { industry },
55 | data: {
56 | ...insights,
57 | lastUpdated: new Date(),
58 | nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
59 | },
60 | });
61 | });
62 | }
63 | }
64 | );
65 |
--------------------------------------------------------------------------------
/app/(main)/interview/_components/stats-cards.jsx:
--------------------------------------------------------------------------------
1 | import { Brain, Target, Trophy } from "lucide-react";
2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3 |
4 | export default function StatsCards({ assessments }) {
5 | const getAverageScore = () => {
6 | if (!assessments?.length) return 0;
7 | const total = assessments.reduce(
8 | (sum, assessment) => sum + assessment.quizScore,
9 | 0
10 | );
11 | return (total / assessments.length).toFixed(1);
12 | };
13 |
14 | const getLatestAssessment = () => {
15 | if (!assessments?.length) return null;
16 | return assessments[0];
17 | };
18 |
19 | const getTotalQuestions = () => {
20 | if (!assessments?.length) return 0;
21 | return assessments.reduce(
22 | (sum, assessment) => sum + assessment.questions.length,
23 | 0
24 | );
25 | };
26 |
27 | return (
28 |
29 |
30 |
31 | Average Score
32 |
33 |
34 |
35 | {getAverageScore()}%
36 |
37 | Across all assessments
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Questions Practiced
46 |
47 |
48 |
49 |
50 | {getTotalQuestions()}
51 | Total questions
52 |
53 |
54 |
55 |
56 |
57 | Latest Score
58 |
59 |
60 |
61 |
62 | {getLatestAssessment()?.quizScore.toFixed(1) || 0}%
63 |
64 | Most recent quiz
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/app/(main)/interview/_components/quiz-result.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Trophy, CheckCircle2, XCircle } from "lucide-react";
4 | import { Button } from "@/components/ui/button";
5 | import { CardContent, CardFooter } from "@/components/ui/card";
6 | import { Progress } from "@/components/ui/progress";
7 |
8 | export default function QuizResult({
9 | result,
10 | hideStartNew = false,
11 | onStartNew,
12 | }) {
13 | if (!result) return null;
14 |
15 | return (
16 |
17 |
18 |
19 | Quiz Results
20 |
21 |
22 |
23 | {/* Score Overview */}
24 |
25 |
{result.quizScore.toFixed(1)}%
26 |
27 |
28 |
29 | {/* Improvement Tip */}
30 | {result.improvementTip && (
31 |
32 |
Improvement Tip:
33 |
{result.improvementTip}
34 |
35 | )}
36 |
37 | {/* Questions Review */}
38 |
39 |
Question Review
40 | {result.questions.map((q, index) => (
41 |
42 |
43 |
{q.question}
44 | {q.isCorrect ? (
45 |
46 | ) : (
47 |
48 | )}
49 |
50 |
51 |
Your answer: {q.userAnswer}
52 | {!q.isCorrect &&
Correct answer: {q.answer}
}
53 |
54 |
55 |
Explanation:
56 |
{q.explanation}
57 |
58 |
59 | ))}
60 |
61 |
62 |
63 | {!hideStartNew && (
64 |
65 |
66 | Start New Quiz
67 |
68 |
69 | )}
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/actions/user.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth } from "@clerk/nextjs/server";
5 | import { revalidatePath } from "next/cache";
6 | import { generateAIInsights } from "./dashboard";
7 |
8 | export async function updateUser(data) {
9 | const { userId } = await auth();
10 | if (!userId) throw new Error("Unauthorized");
11 |
12 | const user = await db.user.findUnique({
13 | where: { clerkUserId: userId },
14 | });
15 |
16 | if (!user) throw new Error("User not found");
17 |
18 | try {
19 | // Start a transaction to handle both operations
20 | const result = await db.$transaction(
21 | async (tx) => {
22 | // First check if industry exists
23 | let industryInsight = await tx.industryInsight.findUnique({
24 | where: {
25 | industry: data.industry,
26 | },
27 | });
28 |
29 | // If industry doesn't exist, create it with default values
30 | if (!industryInsight) {
31 | const insights = await generateAIInsights(data.industry);
32 |
33 | industryInsight = await db.industryInsight.create({
34 | data: {
35 | industry: data.industry,
36 | ...insights,
37 | nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
38 | },
39 | });
40 | }
41 |
42 | // Now update the user
43 | const updatedUser = await tx.user.update({
44 | where: {
45 | id: user.id,
46 | },
47 | data: {
48 | industry: data.industry,
49 | experience: data.experience,
50 | bio: data.bio,
51 | skills: data.skills,
52 | },
53 | });
54 |
55 | return { updatedUser, industryInsight };
56 | },
57 | {
58 | timeout: 10000, // default: 5000
59 | }
60 | );
61 |
62 | revalidatePath("/");
63 | return result.user;
64 | } catch (error) {
65 | console.error("Error updating user and industry:", error.message);
66 | throw new Error("Failed to update profile");
67 | }
68 | }
69 |
70 | export async function getUserOnboardingStatus() {
71 | const { userId } = await auth();
72 | if (!userId) throw new Error("Unauthorized");
73 |
74 | const user = await db.user.findUnique({
75 | where: { clerkUserId: userId },
76 | });
77 |
78 | if (!user) throw new Error("User not found");
79 |
80 | try {
81 | const user = await db.user.findUnique({
82 | where: {
83 | clerkUserId: userId,
84 | },
85 | select: {
86 | industry: true,
87 | },
88 | });
89 |
90 | return {
91 | isOnboarded: !!user?.industry,
92 | };
93 | } catch (error) {
94 | console.error("Error checking onboarding status:", error);
95 | throw new Error("Failed to check onboarding status");
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/actions/resume.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth } from "@clerk/nextjs/server";
5 | import { GoogleGenerativeAI } from "@google/generative-ai";
6 | import { revalidatePath } from "next/cache";
7 |
8 | const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
9 | const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
10 |
11 | export async function saveResume(content) {
12 | const { userId } = await auth();
13 | if (!userId) throw new Error("Unauthorized");
14 |
15 | const user = await db.user.findUnique({
16 | where: { clerkUserId: userId },
17 | });
18 |
19 | if (!user) throw new Error("User not found");
20 |
21 | try {
22 | const resume = await db.resume.upsert({
23 | where: {
24 | userId: user.id,
25 | },
26 | update: {
27 | content,
28 | },
29 | create: {
30 | userId: user.id,
31 | content,
32 | },
33 | });
34 |
35 | revalidatePath("/resume");
36 | return resume;
37 | } catch (error) {
38 | console.error("Error saving resume:", error);
39 | throw new Error("Failed to save resume");
40 | }
41 | }
42 |
43 | export async function getResume() {
44 | const { userId } = await auth();
45 | if (!userId) throw new Error("Unauthorized");
46 |
47 | const user = await db.user.findUnique({
48 | where: { clerkUserId: userId },
49 | });
50 |
51 | if (!user) throw new Error("User not found");
52 |
53 | return await db.resume.findUnique({
54 | where: {
55 | userId: user.id,
56 | },
57 | });
58 | }
59 |
60 | export async function improveWithAI({ current, type }) {
61 | const { userId } = await auth();
62 | if (!userId) throw new Error("Unauthorized");
63 |
64 | const user = await db.user.findUnique({
65 | where: { clerkUserId: userId },
66 | include: {
67 | industryInsight: true,
68 | },
69 | });
70 |
71 | if (!user) throw new Error("User not found");
72 |
73 | const prompt = `
74 | As an expert resume writer, improve the following ${type} description for a ${user.industry} professional.
75 | Make it more impactful, quantifiable, and aligned with industry standards.
76 | Current content: "${current}"
77 |
78 | Requirements:
79 | 1. Use action verbs
80 | 2. Include metrics and results where possible
81 | 3. Highlight relevant technical skills
82 | 4. Keep it concise but detailed
83 | 5. Focus on achievements over responsibilities
84 | 6. Use industry-specific keywords
85 |
86 | Format the response as a single paragraph without any additional text or explanations.
87 | `;
88 |
89 | try {
90 | const result = await model.generateContent(prompt);
91 | const response = result.response;
92 | const improvedContent = response.text().trim();
93 | return improvedContent;
94 | } catch (error) {
95 | console.error("Error improving content:", error);
96 | throw new Error("Failed to improve content");
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/(main)/interview/_components/quiz-list.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { format } from "date-fns";
5 | import { useRouter } from "next/navigation";
6 | import { Button } from "@/components/ui/button";
7 | import {
8 | Card,
9 | CardContent,
10 | CardDescription,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card";
14 | import {
15 | Dialog,
16 | DialogContent,
17 | DialogHeader,
18 | DialogTitle,
19 | } from "@/components/ui/dialog";
20 | import QuizResult from "./quiz-result";
21 |
22 | export default function QuizList({ assessments }) {
23 | const router = useRouter();
24 | const [selectedQuiz, setSelectedQuiz] = useState(null);
25 |
26 | return (
27 | <>
28 |
29 |
30 |
31 |
32 |
33 | Recent Quizzes
34 |
35 |
36 | Review your past quiz performance
37 |
38 |
39 |
router.push("/interview/mock")}>
40 | Start New Quiz
41 |
42 |
43 |
44 |
45 |
46 | {assessments?.map((assessment, i) => (
47 |
setSelectedQuiz(assessment)}
51 | >
52 |
53 |
54 | Quiz {i + 1}
55 |
56 |
57 | Score: {assessment.quizScore.toFixed(1)}%
58 |
59 | {format(
60 | new Date(assessment.createdAt),
61 | "MMMM dd, yyyy HH:mm"
62 | )}
63 |
64 |
65 |
66 | {assessment.improvementTip && (
67 |
68 |
69 | {assessment.improvementTip}
70 |
71 |
72 | )}
73 |
74 | ))}
75 |
76 |
77 |
78 |
79 | setSelectedQuiz(null)}>
80 |
81 |
82 |
83 |
84 | router.push("/interview/mock")}
88 | />
89 |
90 |
91 | >
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | @layer base {
10 | :root {
11 | --background: 0 0% 100%;
12 | --foreground: 0 0% 3.9%;
13 | --card: 0 0% 100%;
14 | --card-foreground: 0 0% 3.9%;
15 | --popover: 0 0% 100%;
16 | --popover-foreground: 0 0% 3.9%;
17 | --primary: 0 0% 9%;
18 | --primary-foreground: 0 0% 98%;
19 | --secondary: 0 0% 96.1%;
20 | --secondary-foreground: 0 0% 9%;
21 | --muted: 0 0% 96.1%;
22 | --muted-foreground: 0 0% 45.1%;
23 | --accent: 0 0% 96.1%;
24 | --accent-foreground: 0 0% 9%;
25 | --destructive: 0 84.2% 60.2%;
26 | --destructive-foreground: 0 0% 98%;
27 | --border: 0 0% 89.8%;
28 | --input: 0 0% 89.8%;
29 | --ring: 0 0% 3.9%;
30 | --chart-1: 12 76% 61%;
31 | --chart-2: 173 58% 39%;
32 | --chart-3: 197 37% 24%;
33 | --chart-4: 43 74% 66%;
34 | --chart-5: 27 87% 67%;
35 | --radius: 0.5rem;
36 | }
37 | .dark {
38 | --background: 0 0% 3.9%;
39 | --foreground: 0 0% 98%;
40 | --card: 0 0% 3.9%;
41 | --card-foreground: 0 0% 98%;
42 | --popover: 0 0% 3.9%;
43 | --popover-foreground: 0 0% 98%;
44 | --primary: 0 0% 98%;
45 | --primary-foreground: 0 0% 9%;
46 | --secondary: 0 0% 14.9%;
47 | --secondary-foreground: 0 0% 98%;
48 | --muted: 0 0% 14.9%;
49 | --muted-foreground: 0 0% 63.9%;
50 | --accent: 0 0% 14.9%;
51 | --accent-foreground: 0 0% 98%;
52 | --destructive: 0 62.8% 30.6%;
53 | --destructive-foreground: 0 0% 98%;
54 | --border: 0 0% 14.9%;
55 | --input: 0 0% 14.9%;
56 | --ring: 0 0% 83.1%;
57 | --chart-1: 220 70% 50%;
58 | --chart-2: 160 60% 45%;
59 | --chart-3: 30 80% 55%;
60 | --chart-4: 280 65% 60%;
61 | --chart-5: 340 75% 55%;
62 | }
63 | }
64 |
65 | @layer base {
66 | * {
67 | @apply border-border;
68 | }
69 | body {
70 | @apply bg-background text-foreground;
71 | }
72 | }
73 |
74 | @layer utilities {
75 | .gradient {
76 | @apply bg-gradient-to-b from-gray-400 via-gray-200 to-gray-600;
77 | }
78 | .gradient-title {
79 | @apply gradient font-extrabold tracking-tighter text-transparent bg-clip-text pb-2 pr-2;
80 | }
81 | }
82 |
83 | .hero-image-wrapper {
84 | perspective: 1000px;
85 | }
86 |
87 | .hero-image {
88 | /* transform: rotateX(20deg) scale(0.9) translateY(-50); */
89 | transform: rotateX(15deg) scale(1);
90 | transition: transform 0.5s ease-out;
91 | will-change: transform;
92 | }
93 |
94 | .hero-image.scrolled {
95 | transform: rotateX(0deg) scale(1) translateY(40px);
96 | }
97 |
98 | .grid-background {
99 | position: fixed;
100 | top: 0;
101 | left: 0;
102 | width: 100%;
103 | height: 100%;
104 | background: linear-gradient(
105 | to right,
106 | rgba(255, 255, 255, 0.1) 1px,
107 | transparent 1px
108 | ),
109 | linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
110 | background-size: 50px 50px;
111 | pointer-events: none;
112 | z-index: -1;
113 | }
114 |
115 | .grid-background::before {
116 | content: "";
117 | position: absolute;
118 | top: 0;
119 | left: 0;
120 | width: 100%;
121 | height: 100%;
122 | background: radial-gradient(circle, transparent, rgba(0, 0, 0, 0.9));
123 | }
124 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id String @id @default(uuid())
12 | clerkUserId String @unique // clerk user id
13 | email String @unique
14 | name String?
15 | imageUrl String?
16 | industry String? // Combined industry-subindustry (e.g., "tech-software-development")
17 | industryInsight IndustryInsight? @relation(fields: [industry], references: [industry])
18 | createdAt DateTime @default(now())
19 | updatedAt DateTime @updatedAt
20 |
21 | // Profile fields
22 | bio String?
23 | experience Int? // Years of experience
24 |
25 | // Relations
26 | skills String[] // Array of skills
27 | assessments Assessment[]
28 | resume Resume?
29 | coverLetter CoverLetter[]
30 | }
31 |
32 | model Assessment {
33 | id String @id @default(cuid())
34 | userId String
35 | user User @relation(fields: [userId], references: [id])
36 | quizScore Float // Overall quiz score
37 | questions Json[] // Array of {question, answer, userAnswer, isCorrect}
38 | category String // "Technical", "Behavioral", etc.
39 | improvementTip String? // AI-generated improvement tip
40 | createdAt DateTime @default(now())
41 | updatedAt DateTime @updatedAt
42 |
43 | @@index([userId])
44 | }
45 |
46 | model Resume {
47 | id String @id @default(cuid())
48 | userId String @unique // One resume per user
49 | user User @relation(fields: [userId], references: [id])
50 | content String @db.Text // Markdown content
51 | atsScore Float?
52 | feedback String?
53 | createdAt DateTime @default(now())
54 | updatedAt DateTime @updatedAt
55 | }
56 |
57 | model CoverLetter {
58 | id String @id @default(cuid())
59 | userId String
60 | user User @relation(fields: [userId], references: [id])
61 | content String // Markdown content
62 | jobDescription String?
63 | companyName String // Name of the company applying to
64 | jobTitle String // Position applying for
65 | status String @default("draft") // draft, completed
66 | createdAt DateTime @default(now())
67 | updatedAt DateTime @updatedAt
68 |
69 | @@index([userId])
70 | }
71 |
72 | // Combined Industry Trends and Salary Insights
73 | model IndustryInsight {
74 | id String @id @default(cuid())
75 | industry String @unique // The industry this data belongs to (e.g., "tech-software-development")
76 |
77 | // Users in this industry
78 | users User[]
79 |
80 | // Salary data
81 | salaryRanges Json[] // Array of { role: string, min: float, max: float, median: float, location: string? }
82 |
83 | // Industry trends
84 | growthRate Float // Industry growth rate
85 | demandLevel String // "High", "Medium", "Low"
86 | topSkills String[] // Most in-demand skills
87 |
88 | // Market conditions
89 | marketOutlook String // "Positive", "Neutral", "Negative"
90 | keyTrends String[] // Array of current industry trends
91 |
92 | // Learning suggestions
93 | recommendedSkills String[] // Skills recommended for the industry
94 |
95 | lastUpdated DateTime @default(now())
96 | nextUpdate DateTime // Scheduled update time
97 |
98 | @@index([industry])
99 | }
--------------------------------------------------------------------------------
/components/ui/dialog.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
18 |
25 | ))
26 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
27 |
28 | const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
29 |
30 |
31 |
38 | {children}
39 |
41 |
42 | Close
43 |
44 |
45 |
46 | ))
47 | DialogContent.displayName = DialogPrimitive.Content.displayName
48 |
49 | const DialogHeader = ({
50 | className,
51 | ...props
52 | }) => (
53 |
56 | )
57 | DialogHeader.displayName = "DialogHeader"
58 |
59 | const DialogFooter = ({
60 | className,
61 | ...props
62 | }) => (
63 |
66 | )
67 | DialogFooter.displayName = "DialogFooter"
68 |
69 | const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
70 |
74 | ))
75 | DialogTitle.displayName = DialogPrimitive.Title.displayName
76 |
77 | const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
78 |
82 | ))
83 | DialogDescription.displayName = DialogPrimitive.Description.displayName
84 |
85 | export {
86 | Dialog,
87 | DialogPortal,
88 | DialogOverlay,
89 | DialogTrigger,
90 | DialogClose,
91 | DialogContent,
92 | DialogHeader,
93 | DialogFooter,
94 | DialogTitle,
95 | DialogDescription,
96 | }
97 |
--------------------------------------------------------------------------------
/actions/cover-letter.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth } from "@clerk/nextjs/server";
5 | import { GoogleGenerativeAI } from "@google/generative-ai";
6 |
7 | const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
8 | const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
9 |
10 | export async function generateCoverLetter(data) {
11 | const { userId } = await auth();
12 | if (!userId) throw new Error("Unauthorized");
13 |
14 | const user = await db.user.findUnique({
15 | where: { clerkUserId: userId },
16 | });
17 |
18 | if (!user) throw new Error("User not found");
19 |
20 | const prompt = `
21 | Write a professional cover letter for a ${data.jobTitle} position at ${
22 | data.companyName
23 | }.
24 |
25 | About the candidate:
26 | - Industry: ${user.industry}
27 | - Years of Experience: ${user.experience}
28 | - Skills: ${user.skills?.join(", ")}
29 | - Professional Background: ${user.bio}
30 |
31 | Job Description:
32 | ${data.jobDescription}
33 |
34 | Requirements:
35 | 1. Use a professional, enthusiastic tone
36 | 2. Highlight relevant skills and experience
37 | 3. Show understanding of the company's needs
38 | 4. Keep it concise (max 400 words)
39 | 5. Use proper business letter formatting in markdown
40 | 6. Include specific examples of achievements
41 | 7. Relate candidate's background to job requirements
42 |
43 | Format the letter in markdown.
44 | `;
45 |
46 | try {
47 | const result = await model.generateContent(prompt);
48 | const content = result.response.text().trim();
49 |
50 | const coverLetter = await db.coverLetter.create({
51 | data: {
52 | content,
53 | jobDescription: data.jobDescription,
54 | companyName: data.companyName,
55 | jobTitle: data.jobTitle,
56 | status: "completed",
57 | userId: user.id,
58 | },
59 | });
60 |
61 | return coverLetter;
62 | } catch (error) {
63 | console.error("Error generating cover letter:", error.message);
64 | throw new Error("Failed to generate cover letter");
65 | }
66 | }
67 |
68 | export async function getCoverLetters() {
69 | const { userId } = await auth();
70 | if (!userId) throw new Error("Unauthorized");
71 |
72 | const user = await db.user.findUnique({
73 | where: { clerkUserId: userId },
74 | });
75 |
76 | if (!user) throw new Error("User not found");
77 |
78 | return await db.coverLetter.findMany({
79 | where: {
80 | userId: user.id,
81 | },
82 | orderBy: {
83 | createdAt: "desc",
84 | },
85 | });
86 | }
87 |
88 | export async function getCoverLetter(id) {
89 | const { userId } = await auth();
90 | if (!userId) throw new Error("Unauthorized");
91 |
92 | const user = await db.user.findUnique({
93 | where: { clerkUserId: userId },
94 | });
95 |
96 | if (!user) throw new Error("User not found");
97 |
98 | return await db.coverLetter.findUnique({
99 | where: {
100 | id,
101 | userId: user.id,
102 | },
103 | });
104 | }
105 |
106 | export async function deleteCoverLetter(id) {
107 | const { userId } = await auth();
108 | if (!userId) throw new Error("Unauthorized");
109 |
110 | const user = await db.user.findUnique({
111 | where: { clerkUserId: userId },
112 | });
113 |
114 | if (!user) throw new Error("User not found");
115 |
116 | return await db.coverLetter.delete({
117 | where: {
118 | id,
119 | userId: user.id,
120 | },
121 | });
122 | }
123 |
--------------------------------------------------------------------------------
/prisma/migrations/20250114060115_create_models/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL,
4 | "clerkUserId" TEXT NOT NULL,
5 | "email" TEXT NOT NULL,
6 | "name" TEXT,
7 | "imageUrl" TEXT,
8 | "industry" TEXT NOT NULL,
9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10 | "updatedAt" TIMESTAMP(3) NOT NULL,
11 | "title" TEXT,
12 | "bio" TEXT,
13 | "experience" INTEGER,
14 | "skills" TEXT[],
15 |
16 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
17 | );
18 |
19 | -- CreateTable
20 | CREATE TABLE "Assessment" (
21 | "id" TEXT NOT NULL,
22 | "userId" TEXT NOT NULL,
23 | "question" TEXT NOT NULL,
24 | "answer" TEXT NOT NULL,
25 | "userAnswer" TEXT NOT NULL,
26 | "score" DOUBLE PRECISION NOT NULL,
27 | "category" TEXT NOT NULL,
28 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
29 | "updatedAt" TIMESTAMP(3) NOT NULL,
30 |
31 | CONSTRAINT "Assessment_pkey" PRIMARY KEY ("id")
32 | );
33 |
34 | -- CreateTable
35 | CREATE TABLE "Resume" (
36 | "id" TEXT NOT NULL,
37 | "userId" TEXT NOT NULL,
38 | "content" TEXT NOT NULL,
39 | "atsScore" DOUBLE PRECISION,
40 | "feedback" TEXT,
41 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
42 | "updatedAt" TIMESTAMP(3) NOT NULL,
43 |
44 | CONSTRAINT "Resume_pkey" PRIMARY KEY ("id")
45 | );
46 |
47 | -- CreateTable
48 | CREATE TABLE "CoverLetter" (
49 | "id" TEXT NOT NULL,
50 | "userId" TEXT NOT NULL,
51 | "content" TEXT NOT NULL,
52 | "jobDescription" TEXT,
53 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
54 | "updatedAt" TIMESTAMP(3) NOT NULL,
55 |
56 | CONSTRAINT "CoverLetter_pkey" PRIMARY KEY ("id")
57 | );
58 |
59 | -- CreateTable
60 | CREATE TABLE "IndustryInsight" (
61 | "id" TEXT NOT NULL,
62 | "industry" TEXT NOT NULL,
63 | "salaryRanges" JSONB[],
64 | "growthRate" DOUBLE PRECISION NOT NULL,
65 | "demandLevel" TEXT NOT NULL,
66 | "topSkills" TEXT[],
67 | "marketOutlook" TEXT NOT NULL,
68 | "keyTrends" TEXT[],
69 | "recommendedSkills" TEXT[],
70 | "lastUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
71 | "nextUpdate" TIMESTAMP(3) NOT NULL,
72 |
73 | CONSTRAINT "IndustryInsight_pkey" PRIMARY KEY ("id")
74 | );
75 |
76 | -- CreateIndex
77 | CREATE UNIQUE INDEX "User_clerkUserId_key" ON "User"("clerkUserId");
78 |
79 | -- CreateIndex
80 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
81 |
82 | -- CreateIndex
83 | CREATE INDEX "Assessment_userId_idx" ON "Assessment"("userId");
84 |
85 | -- CreateIndex
86 | CREATE UNIQUE INDEX "Resume_userId_key" ON "Resume"("userId");
87 |
88 | -- CreateIndex
89 | CREATE UNIQUE INDEX "CoverLetter_userId_key" ON "CoverLetter"("userId");
90 |
91 | -- CreateIndex
92 | CREATE UNIQUE INDEX "IndustryInsight_industry_key" ON "IndustryInsight"("industry");
93 |
94 | -- CreateIndex
95 | CREATE INDEX "IndustryInsight_industry_idx" ON "IndustryInsight"("industry");
96 |
97 | -- AddForeignKey
98 | ALTER TABLE "User" ADD CONSTRAINT "User_industry_fkey" FOREIGN KEY ("industry") REFERENCES "IndustryInsight"("industry") ON DELETE RESTRICT ON UPDATE CASCADE;
99 |
100 | -- AddForeignKey
101 | ALTER TABLE "Assessment" ADD CONSTRAINT "Assessment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
102 |
103 | -- AddForeignKey
104 | ALTER TABLE "Resume" ADD CONSTRAINT "Resume_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
105 |
106 | -- AddForeignKey
107 | ALTER TABLE "CoverLetter" ADD CONSTRAINT "CoverLetter_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
108 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
16 |
23 | ))
24 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
25 |
26 | const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
27 |
28 |
29 |
36 |
37 | ))
38 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
39 |
40 | const AlertDialogHeader = ({
41 | className,
42 | ...props
43 | }) => (
44 |
47 | )
48 | AlertDialogHeader.displayName = "AlertDialogHeader"
49 |
50 | const AlertDialogFooter = ({
51 | className,
52 | ...props
53 | }) => (
54 |
57 | )
58 | AlertDialogFooter.displayName = "AlertDialogFooter"
59 |
60 | const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
61 |
62 | ))
63 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
64 |
65 | const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
66 |
70 | ))
71 | AlertDialogDescription.displayName =
72 | AlertDialogPrimitive.Description.displayName
73 |
74 | const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
75 |
76 | ))
77 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
78 |
79 | const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
80 |
84 | ))
85 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
86 |
87 | export {
88 | AlertDialog,
89 | AlertDialogPortal,
90 | AlertDialogOverlay,
91 | AlertDialogTrigger,
92 | AlertDialogContent,
93 | AlertDialogHeader,
94 | AlertDialogFooter,
95 | AlertDialogTitle,
96 | AlertDialogDescription,
97 | AlertDialogAction,
98 | AlertDialogCancel,
99 | }
100 |
--------------------------------------------------------------------------------
/components/header.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "./ui/button";
3 | import {
4 | PenBox,
5 | LayoutDashboard,
6 | FileText,
7 | GraduationCap,
8 | ChevronDown,
9 | StarsIcon,
10 | } from "lucide-react";
11 | import Link from "next/link";
12 | import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/nextjs";
13 | import {
14 | DropdownMenu,
15 | DropdownMenuContent,
16 | DropdownMenuItem,
17 | DropdownMenuTrigger,
18 | } from "@/components/ui/dropdown-menu";
19 | import Image from "next/image";
20 | import { checkUser } from "@/lib/checkUser";
21 |
22 | export default async function Header() {
23 | await checkUser();
24 |
25 | return (
26 |
27 |
28 |
29 |
36 |
37 |
38 | {/* Action Buttons */}
39 |
40 |
41 |
42 |
46 |
47 | Industry Insights
48 |
49 |
50 |
51 |
52 |
53 |
54 | {/* Growth Tools Dropdown */}
55 |
56 |
57 |
58 |
59 | Growth Tools
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | Build Resume
68 |
69 |
70 |
71 |
75 |
76 | Cover Letter
77 |
78 |
79 |
80 |
81 |
82 | Interview Prep
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | Sign In
92 |
93 |
94 |
95 |
96 |
106 |
107 |
108 |
109 |
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/app/(main)/ai-cover-letter/_components/cover-letter-list.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { format } from "date-fns";
5 | import { Edit2, Eye, Trash2 } from "lucide-react";
6 | import { toast } from "sonner";
7 | import {
8 | Card,
9 | CardContent,
10 | CardDescription,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card";
14 | import { Button } from "@/components/ui/button";
15 | import {
16 | AlertDialog,
17 | AlertDialogAction,
18 | AlertDialogCancel,
19 | AlertDialogContent,
20 | AlertDialogDescription,
21 | AlertDialogFooter,
22 | AlertDialogHeader,
23 | AlertDialogTitle,
24 | AlertDialogTrigger,
25 | } from "@/components/ui/alert-dialog";
26 | import { deleteCoverLetter } from "@/actions/cover-letter";
27 |
28 | export default function CoverLetterList({ coverLetters }) {
29 | const router = useRouter();
30 |
31 | const handleDelete = async (id) => {
32 | try {
33 | await deleteCoverLetter(id);
34 | toast.success("Cover letter deleted successfully!");
35 | router.refresh();
36 | } catch (error) {
37 | toast.error(error.message || "Failed to delete cover letter");
38 | }
39 | };
40 |
41 | if (!coverLetters?.length) {
42 | return (
43 |
44 |
45 | No Cover Letters Yet
46 |
47 | Create your first cover letter to get started
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | return (
55 |
56 | {coverLetters.map((letter) => (
57 |
58 |
59 |
60 |
61 |
62 | {letter.jobTitle} at {letter.companyName}
63 |
64 |
65 | Created {format(new Date(letter.createdAt), "PPP")}
66 |
67 |
68 |
69 |
70 | router.push(`/ai-cover-letter/${letter.id}`)}
74 | >
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | Delete Cover Letter?
85 |
86 | This action cannot be undone. This will permanently
87 | delete your cover letter for {letter.jobTitle} at{" "}
88 | {letter.companyName}.
89 |
90 |
91 |
92 | Cancel
93 | handleDelete(letter.id)}
95 | className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
96 | >
97 | Delete
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | {letter.jobDescription}
108 |
109 |
110 |
111 | ))}
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/app/(main)/ai-cover-letter/_components/cover-letter-generator.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useForm } from "react-hook-form";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { toast } from "sonner";
7 | import { Loader2 } from "lucide-react";
8 | import { Button } from "@/components/ui/button";
9 | import {
10 | Card,
11 | CardContent,
12 | CardDescription,
13 | CardHeader,
14 | CardTitle,
15 | } from "@/components/ui/card";
16 | import { Input } from "@/components/ui/input";
17 | import { Label } from "@/components/ui/label";
18 | import { Textarea } from "@/components/ui/textarea";
19 | import { generateCoverLetter } from "@/actions/cover-letter";
20 | import useFetch from "@/hooks/use-fetch";
21 | import { coverLetterSchema } from "@/app/lib/schema";
22 | import { useEffect } from "react";
23 | import { useRouter } from "next/navigation";
24 |
25 | export default function CoverLetterGenerator() {
26 | const router = useRouter();
27 |
28 | const {
29 | register,
30 | handleSubmit,
31 | formState: { errors },
32 | reset,
33 | } = useForm({
34 | resolver: zodResolver(coverLetterSchema),
35 | });
36 |
37 | const {
38 | loading: generating,
39 | fn: generateLetterFn,
40 | data: generatedLetter,
41 | } = useFetch(generateCoverLetter);
42 |
43 | // Update content when letter is generated
44 | useEffect(() => {
45 | if (generatedLetter) {
46 | toast.success("Cover letter generated successfully!");
47 | router.push(`/ai-cover-letter/${generatedLetter.id}`);
48 | reset();
49 | }
50 | }, [generatedLetter]);
51 |
52 | const onSubmit = async (data) => {
53 | try {
54 | await generateLetterFn(data);
55 | } catch (error) {
56 | toast.error(error.message || "Failed to generate cover letter");
57 | }
58 | };
59 |
60 | return (
61 |
62 |
63 |
64 | Job Details
65 |
66 | Provide information about the position you're applying for
67 |
68 |
69 |
70 |
130 |
131 |
132 |
133 | );
134 | }
135 |
--------------------------------------------------------------------------------
/actions/interview.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/prisma";
4 | import { auth } from "@clerk/nextjs/server";
5 | import { GoogleGenerativeAI } from "@google/generative-ai";
6 |
7 | const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
8 | const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
9 |
10 | export async function generateQuiz() {
11 | const { userId } = await auth();
12 | if (!userId) throw new Error("Unauthorized");
13 |
14 | const user = await db.user.findUnique({
15 | where: { clerkUserId: userId },
16 | select: {
17 | industry: true,
18 | skills: true,
19 | },
20 | });
21 |
22 | if (!user) throw new Error("User not found");
23 |
24 | const prompt = `
25 | Generate 10 technical interview questions for a ${
26 | user.industry
27 | } professional${
28 | user.skills?.length ? ` with expertise in ${user.skills.join(", ")}` : ""
29 | }.
30 |
31 | Each question should be multiple choice with 4 options.
32 |
33 | Return the response in this JSON format only, no additional text:
34 | {
35 | "questions": [
36 | {
37 | "question": "string",
38 | "options": ["string", "string", "string", "string"],
39 | "correctAnswer": "string",
40 | "explanation": "string"
41 | }
42 | ]
43 | }
44 | `;
45 |
46 | try {
47 | const result = await model.generateContent(prompt);
48 | const response = result.response;
49 | const text = response.text();
50 | const cleanedText = text.replace(/```(?:json)?\n?/g, "").trim();
51 | const quiz = JSON.parse(cleanedText);
52 |
53 | return quiz.questions;
54 | } catch (error) {
55 | console.error("Error generating quiz:", error);
56 | throw new Error("Failed to generate quiz questions");
57 | }
58 | }
59 |
60 | export async function saveQuizResult(questions, answers, score) {
61 | const { userId } = await auth();
62 | if (!userId) throw new Error("Unauthorized");
63 |
64 | const user = await db.user.findUnique({
65 | where: { clerkUserId: userId },
66 | });
67 |
68 | if (!user) throw new Error("User not found");
69 |
70 | const questionResults = questions.map((q, index) => ({
71 | question: q.question,
72 | answer: q.correctAnswer,
73 | userAnswer: answers[index],
74 | isCorrect: q.correctAnswer === answers[index],
75 | explanation: q.explanation,
76 | }));
77 |
78 | // Get wrong answers
79 | const wrongAnswers = questionResults.filter((q) => !q.isCorrect);
80 |
81 | // Only generate improvement tips if there are wrong answers
82 | let improvementTip = null;
83 | if (wrongAnswers.length > 0) {
84 | const wrongQuestionsText = wrongAnswers
85 | .map(
86 | (q) =>
87 | `Question: "${q.question}"\nCorrect Answer: "${q.answer}"\nUser Answer: "${q.userAnswer}"`
88 | )
89 | .join("\n\n");
90 |
91 | const improvementPrompt = `
92 | The user got the following ${user.industry} technical interview questions wrong:
93 |
94 | ${wrongQuestionsText}
95 |
96 | Based on these mistakes, provide a concise, specific improvement tip.
97 | Focus on the knowledge gaps revealed by these wrong answers.
98 | Keep the response under 2 sentences and make it encouraging.
99 | Don't explicitly mention the mistakes, instead focus on what to learn/practice.
100 | `;
101 |
102 | try {
103 | const tipResult = await model.generateContent(improvementPrompt);
104 |
105 | improvementTip = tipResult.response.text().trim();
106 | console.log(improvementTip);
107 | } catch (error) {
108 | console.error("Error generating improvement tip:", error);
109 | // Continue without improvement tip if generation fails
110 | }
111 | }
112 |
113 | try {
114 | const assessment = await db.assessment.create({
115 | data: {
116 | userId: user.id,
117 | quizScore: score,
118 | questions: questionResults,
119 | category: "Technical",
120 | improvementTip,
121 | },
122 | });
123 |
124 | return assessment;
125 | } catch (error) {
126 | console.error("Error saving quiz result:", error);
127 | throw new Error("Failed to save quiz result");
128 | }
129 | }
130 |
131 | export async function getAssessments() {
132 | const { userId } = await auth();
133 | if (!userId) throw new Error("Unauthorized");
134 |
135 | const user = await db.user.findUnique({
136 | where: { clerkUserId: userId },
137 | });
138 |
139 | if (!user) throw new Error("User not found");
140 |
141 | try {
142 | const assessments = await db.assessment.findMany({
143 | where: {
144 | userId: user.id,
145 | },
146 | orderBy: {
147 | createdAt: "asc",
148 | },
149 | });
150 |
151 | return assessments;
152 | } catch (error) {
153 | console.error("Error fetching assessments:", error);
154 | throw new Error("Failed to fetch assessments");
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/components/ui/select.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
16 | span]:line-clamp-1",
20 | className
21 | )}
22 | {...props}>
23 | {children}
24 |
25 |
26 |
27 |
28 | ))
29 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
30 |
31 | const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
32 |
36 |
37 |
38 | ))
39 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
40 |
41 | const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
42 |
46 |
47 |
48 | ))
49 | SelectScrollDownButton.displayName =
50 | SelectPrimitive.ScrollDownButton.displayName
51 |
52 | const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
53 |
54 |
64 |
65 |
68 | {children}
69 |
70 |
71 |
72 |
73 | ))
74 | SelectContent.displayName = SelectPrimitive.Content.displayName
75 |
76 | const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
77 |
81 | ))
82 | SelectLabel.displayName = SelectPrimitive.Label.displayName
83 |
84 | const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
85 |
92 |
93 |
94 |
95 |
96 |
97 | {children}
98 |
99 | ))
100 | SelectItem.displayName = SelectPrimitive.Item.displayName
101 |
102 | const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
103 |
107 | ))
108 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
109 |
110 | export {
111 | Select,
112 | SelectGroup,
113 | SelectValue,
114 | SelectTrigger,
115 | SelectContent,
116 | SelectLabel,
117 | SelectItem,
118 | SelectSeparator,
119 | SelectScrollUpButton,
120 | SelectScrollDownButton,
121 | }
122 |
--------------------------------------------------------------------------------
/app/(main)/interview/_components/quiz.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { toast } from "sonner";
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | Card,
8 | CardContent,
9 | CardFooter,
10 | CardHeader,
11 | CardTitle,
12 | } from "@/components/ui/card";
13 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
14 | import { Label } from "@/components/ui/label";
15 | import { generateQuiz, saveQuizResult } from "@/actions/interview";
16 | import QuizResult from "./quiz-result";
17 | import useFetch from "@/hooks/use-fetch";
18 | import { BarLoader } from "react-spinners";
19 |
20 | export default function Quiz() {
21 | const [currentQuestion, setCurrentQuestion] = useState(0);
22 | const [answers, setAnswers] = useState([]);
23 | const [showExplanation, setShowExplanation] = useState(false);
24 |
25 | const {
26 | loading: generatingQuiz,
27 | fn: generateQuizFn,
28 | data: quizData,
29 | } = useFetch(generateQuiz);
30 |
31 | const {
32 | loading: savingResult,
33 | fn: saveQuizResultFn,
34 | data: resultData,
35 | setData: setResultData,
36 | } = useFetch(saveQuizResult);
37 |
38 | useEffect(() => {
39 | if (quizData) {
40 | setAnswers(new Array(quizData.length).fill(null));
41 | }
42 | }, [quizData]);
43 |
44 | const handleAnswer = (answer) => {
45 | const newAnswers = [...answers];
46 | newAnswers[currentQuestion] = answer;
47 | setAnswers(newAnswers);
48 | };
49 |
50 | const handleNext = () => {
51 | if (currentQuestion < quizData.length - 1) {
52 | setCurrentQuestion(currentQuestion + 1);
53 | setShowExplanation(false);
54 | } else {
55 | finishQuiz();
56 | }
57 | };
58 |
59 | const calculateScore = () => {
60 | let correct = 0;
61 | answers.forEach((answer, index) => {
62 | if (answer === quizData[index].correctAnswer) {
63 | correct++;
64 | }
65 | });
66 | return (correct / quizData.length) * 100;
67 | };
68 |
69 | const finishQuiz = async () => {
70 | const score = calculateScore();
71 | try {
72 | await saveQuizResultFn(quizData, answers, score);
73 | toast.success("Quiz completed!");
74 | } catch (error) {
75 | toast.error(error.message || "Failed to save quiz results");
76 | }
77 | };
78 |
79 | const startNewQuiz = () => {
80 | setCurrentQuestion(0);
81 | setAnswers([]);
82 | setShowExplanation(false);
83 | generateQuizFn();
84 | setResultData(null);
85 | };
86 |
87 | if (generatingQuiz) {
88 | return ;
89 | }
90 |
91 | // Show results if quiz is completed
92 | if (resultData) {
93 | return (
94 |
95 |
96 |
97 | );
98 | }
99 |
100 | if (!quizData) {
101 | return (
102 |
103 |
104 | Ready to test your knowledge?
105 |
106 |
107 |
108 | This quiz contains 10 questions specific to your industry and
109 | skills. Take your time and choose the best answer for each question.
110 |
111 |
112 |
113 |
114 | Start Quiz
115 |
116 |
117 |
118 | );
119 | }
120 |
121 | const question = quizData[currentQuestion];
122 |
123 | return (
124 |
125 |
126 |
127 | Question {currentQuestion + 1} of {quizData.length}
128 |
129 |
130 |
131 | {question.question}
132 |
137 | {question.options.map((option, index) => (
138 |
139 |
140 | {option}
141 |
142 | ))}
143 |
144 |
145 | {showExplanation && (
146 |
147 |
Explanation:
148 |
{question.explanation}
149 |
150 | )}
151 |
152 |
153 | {!showExplanation && (
154 | setShowExplanation(true)}
156 | variant="outline"
157 | disabled={!answers[currentQuestion]}
158 | >
159 | Show Explanation
160 |
161 | )}
162 |
167 | {savingResult && (
168 |
169 | )}
170 | {currentQuestion < quizData.length - 1
171 | ? "Next Question"
172 | : "Finish Quiz"}
173 |
174 |
175 |
176 | );
177 | }
178 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
22 |
30 | {children}
31 |
32 |
33 | ))
34 | DropdownMenuSubTrigger.displayName =
35 | DropdownMenuPrimitive.SubTrigger.displayName
36 |
37 | const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
38 |
45 | ))
46 | DropdownMenuSubContent.displayName =
47 | DropdownMenuPrimitive.SubContent.displayName
48 |
49 | const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
50 |
51 |
60 |
61 | ))
62 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
63 |
64 | const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
65 | svg]:size-4 [&>svg]:shrink-0",
69 | inset && "pl-8",
70 | className
71 | )}
72 | {...props} />
73 | ))
74 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
75 |
76 | const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
77 |
85 |
86 |
87 |
88 |
89 |
90 | {children}
91 |
92 | ))
93 | DropdownMenuCheckboxItem.displayName =
94 | DropdownMenuPrimitive.CheckboxItem.displayName
95 |
96 | const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
97 |
104 |
105 |
106 |
107 |
108 |
109 | {children}
110 |
111 | ))
112 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
113 |
114 | const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
115 |
119 | ))
120 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
121 |
122 | const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
123 |
127 | ))
128 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
129 |
130 | const DropdownMenuShortcut = ({
131 | className,
132 | ...props
133 | }) => {
134 | return (
135 | ( )
138 | );
139 | }
140 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
141 |
142 | export {
143 | DropdownMenu,
144 | DropdownMenuTrigger,
145 | DropdownMenuContent,
146 | DropdownMenuItem,
147 | DropdownMenuCheckboxItem,
148 | DropdownMenuRadioItem,
149 | DropdownMenuLabel,
150 | DropdownMenuSeparator,
151 | DropdownMenuShortcut,
152 | DropdownMenuGroup,
153 | DropdownMenuPortal,
154 | DropdownMenuSub,
155 | DropdownMenuSubContent,
156 | DropdownMenuSubTrigger,
157 | DropdownMenuRadioGroup,
158 | }
159 |
--------------------------------------------------------------------------------
/app/(main)/onboarding/_components/onboarding-form.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { useForm } from "react-hook-form";
6 | import { zodResolver } from "@hookform/resolvers/zod";
7 | import { Loader2 } from "lucide-react";
8 | import { toast } from "sonner";
9 | import {
10 | Card,
11 | CardContent,
12 | CardDescription,
13 | CardHeader,
14 | CardTitle,
15 | } from "@/components/ui/card";
16 | import { Button } from "@/components/ui/button";
17 | import { Label } from "@/components/ui/label";
18 | import { Input } from "@/components/ui/input";
19 | import { Textarea } from "@/components/ui/textarea";
20 | import {
21 | Select,
22 | SelectContent,
23 | SelectGroup,
24 | SelectItem,
25 | SelectLabel,
26 | SelectTrigger,
27 | SelectValue,
28 | } from "@/components/ui/select";
29 | import useFetch from "@/hooks/use-fetch";
30 | import { onboardingSchema } from "@/app/lib/schema";
31 | import { updateUser } from "@/actions/user";
32 |
33 | const OnboardingForm = ({ industries }) => {
34 | const router = useRouter();
35 | const [selectedIndustry, setSelectedIndustry] = useState(null);
36 |
37 | const {
38 | loading: updateLoading,
39 | fn: updateUserFn,
40 | data: updateResult,
41 | } = useFetch(updateUser);
42 |
43 | const {
44 | register,
45 | handleSubmit,
46 | formState: { errors },
47 | setValue,
48 | watch,
49 | } = useForm({
50 | resolver: zodResolver(onboardingSchema),
51 | });
52 |
53 | const onSubmit = async (values) => {
54 | try {
55 | const formattedIndustry = `${values.industry}-${values.subIndustry
56 | .toLowerCase()
57 | .replace(/ /g, "-")}`;
58 |
59 | await updateUserFn({
60 | ...values,
61 | industry: formattedIndustry,
62 | });
63 | } catch (error) {
64 | console.error("Onboarding error:", error);
65 | }
66 | };
67 |
68 | useEffect(() => {
69 | if (updateResult?.success && !updateLoading) {
70 | toast.success("Profile completed successfully!");
71 | router.push("/dashboard");
72 | router.refresh();
73 | }
74 | }, [updateResult, updateLoading]);
75 |
76 | const watchIndustry = watch("industry");
77 |
78 | return (
79 |
80 |
81 |
82 |
83 | Complete Your Profile
84 |
85 |
86 | Select your industry to get personalized career insights and
87 | recommendations.
88 |
89 |
90 |
91 |
92 |
93 |
Industry
94 |
{
96 | setValue("industry", value);
97 | setSelectedIndustry(
98 | industries.find((ind) => ind.id === value)
99 | );
100 | setValue("subIndustry", "");
101 | }}
102 | >
103 |
104 |
105 |
106 |
107 |
108 | Industries
109 | {industries.map((ind) => (
110 |
111 | {ind.name}
112 |
113 | ))}
114 |
115 |
116 |
117 | {errors.industry && (
118 |
119 | {errors.industry.message}
120 |
121 | )}
122 |
123 |
124 | {watchIndustry && (
125 |
126 |
Specialization
127 |
setValue("subIndustry", value)}
129 | >
130 |
131 |
132 |
133 |
134 |
135 | Specializations
136 | {selectedIndustry?.subIndustries.map((sub) => (
137 |
138 | {sub}
139 |
140 | ))}
141 |
142 |
143 |
144 | {errors.subIndustry && (
145 |
146 | {errors.subIndustry.message}
147 |
148 | )}
149 |
150 | )}
151 |
152 |
153 |
Years of Experience
154 |
162 | {errors.experience && (
163 |
164 | {errors.experience.message}
165 |
166 | )}
167 |
168 |
169 |
170 |
Skills
171 |
176 |
177 | Separate multiple skills with commas
178 |
179 | {errors.skills && (
180 |
{errors.skills.message}
181 | )}
182 |
183 |
184 |
185 |
Professional Bio
186 |
192 | {errors.bio && (
193 |
{errors.bio.message}
194 | )}
195 |
196 |
197 |
198 | {updateLoading ? (
199 | <>
200 |
201 | Saving...
202 | >
203 | ) : (
204 | "Complete Profile"
205 | )}
206 |
207 |
208 |
209 |
210 |
211 | );
212 | };
213 |
214 | export default OnboardingForm;
215 |
--------------------------------------------------------------------------------
/data/industries.js:
--------------------------------------------------------------------------------
1 | export const industries = [
2 | {
3 | id: "tech",
4 | name: "Technology",
5 | subIndustries: [
6 | "Software Development",
7 | "IT Services",
8 | "Cybersecurity",
9 | "Cloud Computing",
10 | "Artificial Intelligence/Machine Learning",
11 | "Data Science & Analytics",
12 | "Internet & Web Services",
13 | "Robotics",
14 | "Quantum Computing",
15 | "Blockchain & Cryptocurrency",
16 | "IoT (Internet of Things)",
17 | "Virtual/Augmented Reality",
18 | "Semiconductor & Electronics",
19 | ],
20 | },
21 | {
22 | id: "finance",
23 | name: "Financial Services",
24 | subIndustries: [
25 | "Banking",
26 | "Investment Banking",
27 | "Insurance",
28 | "FinTech",
29 | "Wealth Management",
30 | "Asset Management",
31 | "Real Estate Investment",
32 | "Private Equity",
33 | "Venture Capital",
34 | "Cryptocurrency & Digital Assets",
35 | "Risk Management",
36 | "Payment Processing",
37 | "Credit Services",
38 | ],
39 | },
40 | {
41 | id: "healthcare",
42 | name: "Healthcare & Life Sciences",
43 | subIndustries: [
44 | "Healthcare Services",
45 | "Biotechnology",
46 | "Pharmaceuticals",
47 | "Medical Devices",
48 | "Healthcare IT",
49 | "Telemedicine",
50 | "Mental Health Services",
51 | "Genomics",
52 | "Clinical Research",
53 | "Healthcare Analytics",
54 | "Elder Care Services",
55 | "Veterinary Services",
56 | "Alternative Medicine",
57 | ],
58 | },
59 | {
60 | id: "manufacturing",
61 | name: "Manufacturing & Industrial",
62 | subIndustries: [
63 | "Automotive",
64 | "Aerospace & Defense",
65 | "Electronics Manufacturing",
66 | "Industrial Manufacturing",
67 | "Chemical Manufacturing",
68 | "Consumer Goods",
69 | "Food & Beverage Processing",
70 | "Textile Manufacturing",
71 | "Metal Fabrication",
72 | "3D Printing/Additive Manufacturing",
73 | "Machinery & Equipment",
74 | "Packaging",
75 | "Plastics & Rubber",
76 | ],
77 | },
78 | {
79 | id: "retail",
80 | name: "Retail & E-commerce",
81 | subIndustries: [
82 | "E-commerce Platforms",
83 | "Retail Technology",
84 | "Fashion & Apparel",
85 | "Consumer Electronics",
86 | "Grocery & Food Retail",
87 | "Luxury Goods",
88 | "Sports & Recreation",
89 | "Home & Garden",
90 | "Beauty & Personal Care",
91 | "Pet Products",
92 | "Specialty Retail",
93 | "Direct-to-Consumer (D2C)",
94 | "Department Stores",
95 | ],
96 | },
97 | {
98 | id: "media",
99 | name: "Media & Entertainment",
100 | subIndustries: [
101 | "Digital Media",
102 | "Gaming & Esports",
103 | "Streaming Services",
104 | "Social Media",
105 | "Digital Marketing",
106 | "Film & Television",
107 | "Music & Audio",
108 | "Publishing",
109 | "Advertising",
110 | "Sports Entertainment",
111 | "News & Journalism",
112 | "Animation",
113 | "Event Management",
114 | ],
115 | },
116 | {
117 | id: "education",
118 | name: "Education & Training",
119 | subIndustries: [
120 | "EdTech",
121 | "Higher Education",
122 | "Professional Training",
123 | "Online Learning",
124 | "K-12 Education",
125 | "Corporate Training",
126 | "Language Learning",
127 | "Special Education",
128 | "Early Childhood Education",
129 | "Career Development",
130 | "Educational Publishing",
131 | "Educational Consulting",
132 | "Vocational Training",
133 | ],
134 | },
135 | {
136 | id: "energy",
137 | name: "Energy & Utilities",
138 | subIndustries: [
139 | "Renewable Energy",
140 | "Clean Technology",
141 | "Oil & Gas",
142 | "Nuclear Energy",
143 | "Energy Management",
144 | "Utilities",
145 | "Smart Grid Technology",
146 | "Energy Storage",
147 | "Carbon Management",
148 | "Waste Management",
149 | "Water & Wastewater",
150 | "Mining",
151 | "Environmental Services",
152 | ],
153 | },
154 | {
155 | id: "consulting",
156 | name: "Professional Services",
157 | subIndustries: [
158 | "Management Consulting",
159 | "IT Consulting",
160 | "Strategy Consulting",
161 | "Digital Transformation",
162 | "Business Advisory",
163 | "Legal Services",
164 | "Accounting & Tax",
165 | "Human Resources",
166 | "Marketing Services",
167 | "Architecture",
168 | "Engineering Services",
169 | "Research & Development",
170 | "Business Process Outsourcing (BPO)",
171 | ],
172 | },
173 | {
174 | id: "telecom",
175 | name: "Telecommunications",
176 | subIndustries: [
177 | "Wireless Communications",
178 | "Network Infrastructure",
179 | "Telecom Services",
180 | "5G Technology",
181 | "Internet Service Providers",
182 | "Satellite Communications",
183 | "Data Centers",
184 | "Fiber Optics",
185 | "Mobile Technology",
186 | "VoIP Services",
187 | "Network Security",
188 | "Telecom Equipment",
189 | "Cloud Communications",
190 | ],
191 | },
192 | {
193 | id: "transportation",
194 | name: "Transportation & Logistics",
195 | subIndustries: [
196 | "Electric Vehicles",
197 | "Autonomous Vehicles",
198 | "Logistics & Supply Chain",
199 | "Aviation",
200 | "Railways",
201 | "Maritime Transport",
202 | "Urban Mobility",
203 | "Fleet Management",
204 | "Last-Mile Delivery",
205 | "Warehousing",
206 | "Freight & Cargo",
207 | "Public Transportation",
208 | "Space Transportation",
209 | ],
210 | },
211 | {
212 | id: "agriculture",
213 | name: "Agriculture & Food",
214 | subIndustries: [
215 | "AgTech",
216 | "Farming",
217 | "Food Production",
218 | "Sustainable Agriculture",
219 | "Precision Agriculture",
220 | "Aquaculture",
221 | "Vertical Farming",
222 | "Agricultural Biotechnology",
223 | "Food Processing",
224 | "Organic Farming",
225 | "Plant-Based Foods",
226 | "Agricultural Equipment",
227 | "Indoor Farming",
228 | ],
229 | },
230 | {
231 | id: "construction",
232 | name: "Construction & Real Estate",
233 | subIndustries: [
234 | "Commercial Construction",
235 | "Residential Construction",
236 | "Real Estate Development",
237 | "Property Management",
238 | "Construction Technology",
239 | "Building Materials",
240 | "Infrastructure Development",
241 | "Smart Buildings",
242 | "Interior Design",
243 | "Facilities Management",
244 | "Real Estate Technology",
245 | "Sustainable Building",
246 | "Urban Planning",
247 | ],
248 | },
249 | {
250 | id: "hospitality",
251 | name: "Hospitality & Tourism",
252 | subIndustries: [
253 | "Hotels & Resorts",
254 | "Restaurants & Food Service",
255 | "Travel Technology",
256 | "Tourism",
257 | "Event Planning",
258 | "Vacation Rentals",
259 | "Cruise Lines",
260 | "Catering",
261 | "Theme Parks",
262 | "Travel Agencies",
263 | "Hospitality Management",
264 | "Online Travel Booking",
265 | "Cultural Tourism",
266 | ],
267 | },
268 | {
269 | id: "nonprofit",
270 | name: "Non-Profit & Social Services",
271 | subIndustries: [
272 | "Charitable Organizations",
273 | "Social Services",
274 | "Environmental Conservation",
275 | "Humanitarian Aid",
276 | "Education Non-Profits",
277 | "Healthcare Non-Profits",
278 | "Arts & Culture",
279 | "Community Development",
280 | "International Development",
281 | "Animal Welfare",
282 | "Youth Organizations",
283 | "Social Enterprise",
284 | "Advocacy Organizations",
285 | ],
286 | },
287 | ];
288 |
--------------------------------------------------------------------------------
/app/(main)/dashboard/_component/dashboard-view.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import {
5 | BarChart,
6 | Bar,
7 | XAxis,
8 | YAxis,
9 | CartesianGrid,
10 | Tooltip,
11 | ResponsiveContainer,
12 | } from "recharts";
13 | import {
14 | BriefcaseIcon,
15 | LineChart,
16 | TrendingUp,
17 | TrendingDown,
18 | Brain,
19 | } from "lucide-react";
20 | import { format, formatDistanceToNow } from "date-fns";
21 | import {
22 | Card,
23 | CardContent,
24 | CardDescription,
25 | CardHeader,
26 | CardTitle,
27 | } from "@/components/ui/card";
28 | import { Badge } from "@/components/ui/badge";
29 | import { Progress } from "@/components/ui/progress";
30 |
31 | const DashboardView = ({ insights }) => {
32 | // Transform salary data for the chart
33 | const salaryData = insights.salaryRanges.map((range) => ({
34 | name: range.role,
35 | min: range.min / 1000,
36 | max: range.max / 1000,
37 | median: range.median / 1000,
38 | }));
39 |
40 | const getDemandLevelColor = (level) => {
41 | switch (level.toLowerCase()) {
42 | case "high":
43 | return "bg-green-500";
44 | case "medium":
45 | return "bg-yellow-500";
46 | case "low":
47 | return "bg-red-500";
48 | default:
49 | return "bg-gray-500";
50 | }
51 | };
52 |
53 | const getMarketOutlookInfo = (outlook) => {
54 | switch (outlook.toLowerCase()) {
55 | case "positive":
56 | return { icon: TrendingUp, color: "text-green-500" };
57 | case "neutral":
58 | return { icon: LineChart, color: "text-yellow-500" };
59 | case "negative":
60 | return { icon: TrendingDown, color: "text-red-500" };
61 | default:
62 | return { icon: LineChart, color: "text-gray-500" };
63 | }
64 | };
65 |
66 | const OutlookIcon = getMarketOutlookInfo(insights.marketOutlook).icon;
67 | const outlookColor = getMarketOutlookInfo(insights.marketOutlook).color;
68 |
69 | // Format dates using date-fns
70 | const lastUpdatedDate = format(new Date(insights.lastUpdated), "dd/MM/yyyy");
71 | const nextUpdateDistance = formatDistanceToNow(
72 | new Date(insights.nextUpdate),
73 | { addSuffix: true }
74 | );
75 |
76 | return (
77 |
78 |
79 | Last updated: {lastUpdatedDate}
80 |
81 |
82 | {/* Market Overview Cards */}
83 |
84 |
85 |
86 |
87 | Market Outlook
88 |
89 |
90 |
91 |
92 | {insights.marketOutlook}
93 |
94 | Next update {nextUpdateDistance}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | Industry Growth
103 |
104 |
105 |
106 |
107 |
108 | {insights.growthRate.toFixed(1)}%
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | Demand Level
117 |
118 |
119 |
120 | {insights.demandLevel}
121 |
126 |
127 |
128 |
129 |
130 |
131 | Top Skills
132 |
133 |
134 |
135 |
136 | {insights.topSkills.map((skill) => (
137 |
138 | {skill}
139 |
140 | ))}
141 |
142 |
143 |
144 |
145 |
146 | {/* Salary Ranges Chart */}
147 |
148 |
149 | Salary Ranges by Role
150 |
151 | Displaying minimum, median, and maximum salaries (in thousands)
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | {
163 | if (active && payload && payload.length) {
164 | return (
165 |
166 |
{label}
167 | {payload.map((item) => (
168 |
169 | {item.name}: ${item.value}K
170 |
171 | ))}
172 |
173 | );
174 | }
175 | return null;
176 | }}
177 | />
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | {/* Industry Trends */}
188 |
189 |
190 |
191 | Key Industry Trends
192 |
193 | Current trends shaping the industry
194 |
195 |
196 |
197 |
198 | {insights.keyTrends.map((trend, index) => (
199 |
200 |
201 | {trend}
202 |
203 | ))}
204 |
205 |
206 |
207 |
208 |
209 |
210 | Recommended Skills
211 | Skills to consider developing
212 |
213 |
214 |
215 | {insights.recommendedSkills.map((skill) => (
216 |
217 | {skill}
218 |
219 | ))}
220 |
221 |
222 |
223 |
224 |
225 | );
226 | };
227 |
228 | export default DashboardView;
229 |
--------------------------------------------------------------------------------
/app/page.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 | import { Button } from "@/components/ui/button";
4 | import { Card, CardContent } from "@/components/ui/card";
5 | import {
6 | ArrowRight,
7 | Trophy,
8 | Target,
9 | Sparkles,
10 | CheckCircle2,
11 | } from "lucide-react";
12 | import HeroSection from "@/components/hero";
13 | import {
14 | Accordion,
15 | AccordionContent,
16 | AccordionItem,
17 | AccordionTrigger,
18 | } from "@/components/ui/accordion";
19 | import Image from "next/image";
20 | import { features } from "@/data/features";
21 | import { testimonial } from "@/data/testimonial";
22 | import { faqs } from "@/data/faqs";
23 | import { howItWorks } from "@/data/howItWorks";
24 |
25 | export default function LandingPage() {
26 | return (
27 | <>
28 |
29 |
30 | {/* Hero Section */}
31 |
32 |
33 | {/* Features Section */}
34 |
35 |
36 |
37 | Powerful Features for Your Career Growth
38 |
39 |
40 | {features.map((feature, index) => (
41 |
45 |
46 |
47 | {feature.icon}
48 |
{feature.title}
49 |
50 | {feature.description}
51 |
52 |
53 |
54 |
55 | ))}
56 |
57 |
58 |
59 |
60 | {/* Stats Section */}
61 |
62 |
63 |
64 |
65 |
50+
66 |
Industries Covered
67 |
68 |
69 |
1000+
70 |
Interview Questions
71 |
72 |
73 |
95%
74 |
Success Rate
75 |
76 |
77 |
24/7
78 |
AI Support
79 |
80 |
81 |
82 |
83 |
84 | {/* How It Works Section */}
85 |
86 |
87 |
88 |
How It Works
89 |
90 | Four simple steps to accelerate your career growth
91 |
92 |
93 |
94 |
95 | {howItWorks.map((item, index) => (
96 |
100 |
101 | {item.icon}
102 |
103 |
{item.title}
104 |
{item.description}
105 |
106 | ))}
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | What Our Users Say
115 |
116 |
117 | {testimonial.map((testimonial, index) => (
118 |
119 |
120 |
121 |
122 |
123 |
130 |
131 |
132 |
{testimonial.author}
133 |
134 | {testimonial.role}
135 |
136 |
137 | {testimonial.company}
138 |
139 |
140 |
141 |
142 |
143 |
144 | "
145 |
146 | {testimonial.quote}
147 |
148 | "
149 |
150 |
151 |
152 |
153 |
154 |
155 | ))}
156 |
157 |
158 |
159 |
160 | {/* FAQ Section */}
161 |
162 |
163 |
164 |
165 | Frequently Asked Questions
166 |
167 |
168 | Find answers to common questions about our platform
169 |
170 |
171 |
172 |
173 |
174 | {faqs.map((faq, index) => (
175 |
176 |
177 | {faq.question}
178 |
179 | {faq.answer}
180 |
181 | ))}
182 |
183 |
184 |
185 |
186 |
187 | {/* CTA Section */}
188 |
189 |
190 |
191 |
192 | Ready to Accelerate Your Career?
193 |
194 |
195 | Join thousands of professionals who are advancing their careers
196 | with AI-powered guidance.
197 |
198 |
199 |
204 | Start Your Journey Today
205 |
206 |
207 |
208 |
209 |
210 | >
211 | );
212 | }
213 |
--------------------------------------------------------------------------------
/app/(main)/resume/_components/entry-form.jsx:
--------------------------------------------------------------------------------
1 | // app/resume/_components/entry-form.jsx
2 | "use client";
3 |
4 | import { useEffect, useState } from "react";
5 | import { useForm } from "react-hook-form";
6 | import { zodResolver } from "@hookform/resolvers/zod";
7 | import { format, parse } from "date-fns";
8 | import { Button } from "@/components/ui/button";
9 | import { Input } from "@/components/ui/input";
10 | import { Textarea } from "@/components/ui/textarea";
11 | import {
12 | Card,
13 | CardContent,
14 | CardFooter,
15 | CardHeader,
16 | CardTitle,
17 | } from "@/components/ui/card";
18 | import { entrySchema } from "@/app/lib/schema";
19 | import { Sparkles, PlusCircle, X, Pencil, Save, Loader2 } from "lucide-react";
20 | import { improveWithAI } from "@/actions/resume";
21 | import { toast } from "sonner";
22 | import useFetch from "@/hooks/use-fetch";
23 |
24 | const formatDisplayDate = (dateString) => {
25 | if (!dateString) return "";
26 | const date = parse(dateString, "yyyy-MM", new Date());
27 | return format(date, "MMM yyyy");
28 | };
29 |
30 | export function EntryForm({ type, entries, onChange }) {
31 | const [isAdding, setIsAdding] = useState(false);
32 |
33 | const {
34 | register,
35 | handleSubmit: handleValidation,
36 | formState: { errors },
37 | reset,
38 | watch,
39 | setValue,
40 | } = useForm({
41 | resolver: zodResolver(entrySchema),
42 | defaultValues: {
43 | title: "",
44 | organization: "",
45 | startDate: "",
46 | endDate: "",
47 | description: "",
48 | current: false,
49 | },
50 | });
51 |
52 | const current = watch("current");
53 |
54 | const handleAdd = handleValidation((data) => {
55 | const formattedEntry = {
56 | ...data,
57 | startDate: formatDisplayDate(data.startDate),
58 | endDate: data.current ? "" : formatDisplayDate(data.endDate),
59 | };
60 |
61 | onChange([...entries, formattedEntry]);
62 |
63 | reset();
64 | setIsAdding(false);
65 | });
66 |
67 | const handleDelete = (index) => {
68 | const newEntries = entries.filter((_, i) => i !== index);
69 | onChange(newEntries);
70 | };
71 |
72 | const {
73 | loading: isImproving,
74 | fn: improveWithAIFn,
75 | data: improvedContent,
76 | error: improveError,
77 | } = useFetch(improveWithAI);
78 |
79 | // Add this effect to handle the improvement result
80 | useEffect(() => {
81 | if (improvedContent && !isImproving) {
82 | setValue("description", improvedContent);
83 | toast.success("Description improved successfully!");
84 | }
85 | if (improveError) {
86 | toast.error(improveError.message || "Failed to improve description");
87 | }
88 | }, [improvedContent, improveError, isImproving, setValue]);
89 |
90 | // Replace handleImproveDescription with this
91 | const handleImproveDescription = async () => {
92 | const description = watch("description");
93 | if (!description) {
94 | toast.error("Please enter a description first");
95 | return;
96 | }
97 |
98 | await improveWithAIFn({
99 | current: description,
100 | type: type.toLowerCase(), // 'experience', 'education', or 'project'
101 | });
102 | };
103 |
104 | return (
105 |
106 |
107 | {entries.map((item, index) => (
108 |
109 |
110 |
111 | {item.title} @ {item.organization}
112 |
113 | handleDelete(index)}
118 | >
119 |
120 |
121 |
122 |
123 |
124 | {item.current
125 | ? `${item.startDate} - Present`
126 | : `${item.startDate} - ${item.endDate}`}
127 |
128 |
129 | {item.description}
130 |
131 |
132 |
133 | ))}
134 |
135 |
136 | {isAdding && (
137 |
138 |
139 | Add {type}
140 |
141 |
142 |
143 |
144 |
149 | {errors.title && (
150 |
{errors.title.message}
151 | )}
152 |
153 |
154 |
159 | {errors.organization && (
160 |
161 | {errors.organization.message}
162 |
163 | )}
164 |
165 |
166 |
167 |
168 |
169 |
174 | {errors.startDate && (
175 |
176 | {errors.startDate.message}
177 |
178 | )}
179 |
180 |
181 |
187 | {errors.endDate && (
188 |
189 | {errors.endDate.message}
190 |
191 | )}
192 |
193 |
194 |
195 |
196 | {
201 | setValue("current", e.target.checked);
202 | if (e.target.checked) {
203 | setValue("endDate", "");
204 | }
205 | }}
206 | />
207 | Current {type}
208 |
209 |
210 |
211 |
217 | {errors.description && (
218 |
219 | {errors.description.message}
220 |
221 | )}
222 |
223 |
230 | {isImproving ? (
231 | <>
232 |
233 | Improving...
234 | >
235 | ) : (
236 | <>
237 |
238 | Improve with AI
239 | >
240 | )}
241 |
242 |
243 |
244 | {
248 | reset();
249 | setIsAdding(false);
250 | }}
251 | >
252 | Cancel
253 |
254 |
255 |
256 | Add Entry
257 |
258 |
259 |
260 | )}
261 |
262 | {!isAdding && (
263 |
setIsAdding(true)}
267 | >
268 |
269 | Add {type}
270 |
271 | )}
272 |
273 | );
274 | }
275 |
--------------------------------------------------------------------------------
/app/(main)/resume/_components/resume-builder.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { useForm, Controller } from "react-hook-form";
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import {
7 | AlertTriangle,
8 | Download,
9 | Edit,
10 | Loader2,
11 | Monitor,
12 | Save,
13 | } from "lucide-react";
14 | import { toast } from "sonner";
15 | import MDEditor from "@uiw/react-md-editor";
16 | import { Button } from "@/components/ui/button";
17 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
18 | import { Textarea } from "@/components/ui/textarea";
19 | import { Input } from "@/components/ui/input";
20 | import { saveResume } from "@/actions/resume";
21 | import { EntryForm } from "./entry-form";
22 | import useFetch from "@/hooks/use-fetch";
23 | import { useUser } from "@clerk/nextjs";
24 | import { entriesToMarkdown } from "@/app/lib/helper";
25 | import { resumeSchema } from "@/app/lib/schema";
26 | import html2pdf from "html2pdf.js/dist/html2pdf.min.js";
27 |
28 | export default function ResumeBuilder({ initialContent }) {
29 | const [activeTab, setActiveTab] = useState("edit");
30 | const [previewContent, setPreviewContent] = useState(initialContent);
31 | const { user } = useUser();
32 | const [resumeMode, setResumeMode] = useState("preview");
33 |
34 | const {
35 | control,
36 | register,
37 | handleSubmit,
38 | watch,
39 | formState: { errors },
40 | } = useForm({
41 | resolver: zodResolver(resumeSchema),
42 | defaultValues: {
43 | contactInfo: {},
44 | summary: "",
45 | skills: "",
46 | experience: [],
47 | education: [],
48 | projects: [],
49 | },
50 | });
51 |
52 | const {
53 | loading: isSaving,
54 | fn: saveResumeFn,
55 | data: saveResult,
56 | error: saveError,
57 | } = useFetch(saveResume);
58 |
59 | // Watch form fields for preview updates
60 | const formValues = watch();
61 |
62 | useEffect(() => {
63 | if (initialContent) setActiveTab("preview");
64 | }, [initialContent]);
65 |
66 | // Update preview content when form values change
67 | useEffect(() => {
68 | if (activeTab === "edit") {
69 | const newContent = getCombinedContent();
70 | setPreviewContent(newContent ? newContent : initialContent);
71 | }
72 | }, [formValues, activeTab]);
73 |
74 | // Handle save result
75 | useEffect(() => {
76 | if (saveResult && !isSaving) {
77 | toast.success("Resume saved successfully!");
78 | }
79 | if (saveError) {
80 | toast.error(saveError.message || "Failed to save resume");
81 | }
82 | }, [saveResult, saveError, isSaving]);
83 |
84 | const getContactMarkdown = () => {
85 | const { contactInfo } = formValues;
86 | const parts = [];
87 | if (contactInfo.email) parts.push(`📧 ${contactInfo.email}`);
88 | if (contactInfo.mobile) parts.push(`📱 ${contactInfo.mobile}`);
89 | if (contactInfo.linkedin)
90 | parts.push(`💼 [LinkedIn](${contactInfo.linkedin})`);
91 | if (contactInfo.twitter) parts.push(`🐦 [Twitter](${contactInfo.twitter})`);
92 |
93 | return parts.length > 0
94 | ? `## ${user.fullName}
95 | \n\n\n\n${parts.join(" | ")}\n\n
`
96 | : "";
97 | };
98 |
99 | const getCombinedContent = () => {
100 | const { summary, skills, experience, education, projects } = formValues;
101 | return [
102 | getContactMarkdown(),
103 | summary && `## Professional Summary\n\n${summary}`,
104 | skills && `## Skills\n\n${skills}`,
105 | entriesToMarkdown(experience, "Work Experience"),
106 | entriesToMarkdown(education, "Education"),
107 | entriesToMarkdown(projects, "Projects"),
108 | ]
109 | .filter(Boolean)
110 | .join("\n\n");
111 | };
112 |
113 | const [isGenerating, setIsGenerating] = useState(false);
114 |
115 | const generatePDF = async () => {
116 | setIsGenerating(true);
117 | try {
118 | const element = document.getElementById("resume-pdf");
119 | const opt = {
120 | margin: [15, 15],
121 | filename: "resume.pdf",
122 | image: { type: "jpeg", quality: 0.98 },
123 | html2canvas: { scale: 2 },
124 | jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
125 | };
126 |
127 | await html2pdf().set(opt).from(element).save();
128 | } catch (error) {
129 | console.error("PDF generation error:", error);
130 | } finally {
131 | setIsGenerating(false);
132 | }
133 | };
134 |
135 | const onSubmit = async (data) => {
136 | try {
137 | const formattedContent = previewContent
138 | .replace(/\n/g, "\n") // Normalize newlines
139 | .replace(/\n\s*\n/g, "\n\n") // Normalize multiple newlines to double newlines
140 | .trim();
141 |
142 | console.log(previewContent, formattedContent);
143 | await saveResumeFn(previewContent);
144 | } catch (error) {
145 | console.error("Save error:", error);
146 | }
147 | };
148 |
149 | return (
150 |
151 |
152 |
153 | Resume Builder
154 |
155 |
156 |
161 | {isSaving ? (
162 | <>
163 |
164 | Saving...
165 | >
166 | ) : (
167 | <>
168 |
169 | Save
170 | >
171 | )}
172 |
173 |
174 | {isGenerating ? (
175 | <>
176 |
177 | Generating PDF...
178 | >
179 | ) : (
180 | <>
181 |
182 | Download PDF
183 | >
184 | )}
185 |
186 |
187 |
188 |
189 |
190 |
191 | Form
192 | Markdown
193 |
194 |
195 |
196 |
197 | {/* Contact Information */}
198 |
199 |
Contact Information
200 |
201 |
202 |
Email
203 |
209 | {errors.contactInfo?.email && (
210 |
211 | {errors.contactInfo.email.message}
212 |
213 | )}
214 |
215 |
216 |
Mobile Number
217 |
222 | {errors.contactInfo?.mobile && (
223 |
224 | {errors.contactInfo.mobile.message}
225 |
226 | )}
227 |
228 |
229 |
LinkedIn URL
230 |
235 | {errors.contactInfo?.linkedin && (
236 |
237 | {errors.contactInfo.linkedin.message}
238 |
239 | )}
240 |
241 |
242 |
243 | Twitter/X Profile
244 |
245 |
250 | {errors.contactInfo?.twitter && (
251 |
252 | {errors.contactInfo.twitter.message}
253 |
254 | )}
255 |
256 |
257 |
258 |
259 | {/* Summary */}
260 |
261 |
Professional Summary
262 |
(
266 |
272 | )}
273 | />
274 | {errors.summary && (
275 | {errors.summary.message}
276 | )}
277 |
278 |
279 | {/* Skills */}
280 |
281 |
Skills
282 |
(
286 |
292 | )}
293 | />
294 | {errors.skills && (
295 | {errors.skills.message}
296 | )}
297 |
298 |
299 | {/* Experience */}
300 |
301 |
Work Experience
302 |
(
306 |
311 | )}
312 | />
313 | {errors.experience && (
314 |
315 | {errors.experience.message}
316 |
317 | )}
318 |
319 |
320 | {/* Education */}
321 |
322 |
Education
323 |
(
327 |
332 | )}
333 | />
334 | {errors.education && (
335 |
336 | {errors.education.message}
337 |
338 | )}
339 |
340 |
341 | {/* Projects */}
342 |
343 |
Projects
344 |
(
348 |
353 | )}
354 | />
355 | {errors.projects && (
356 |
357 | {errors.projects.message}
358 |
359 | )}
360 |
361 |
362 |
363 |
364 |
365 | {activeTab === "preview" && (
366 |
371 | setResumeMode(resumeMode === "preview" ? "edit" : "preview")
372 | }
373 | >
374 | {resumeMode === "preview" ? (
375 | <>
376 |
377 | Edit Resume
378 | >
379 | ) : (
380 | <>
381 |
382 | Show Preview
383 | >
384 | )}
385 |
386 | )}
387 |
388 | {activeTab === "preview" && resumeMode !== "preview" && (
389 |
390 |
391 |
392 | You will lose editied markdown if you update the form data.
393 |
394 |
395 | )}
396 |
397 |
403 |
404 |
415 |
416 |
417 |
418 | );
419 | }
420 |
--------------------------------------------------------------------------------