├── app ├── favicon.ico ├── (auth) │ ├── sign-in │ │ └── page.tsx │ ├── sign-up │ │ └── page.tsx │ └── layout.tsx ├── (dash) │ ├── interview │ │ ├── page.tsx │ │ └── [id] │ │ │ ├── page.tsx │ │ │ └── feedback │ │ │ └── page.tsx │ ├── layout.tsx │ └── dashboard │ │ └── page.tsx ├── layout.tsx ├── (root) │ ├── layout.tsx │ └── page.tsx ├── api │ └── google-auth │ │ ├── route.ts │ │ └── vapi │ │ └── generate │ │ └── route.ts └── globals.css ├── public ├── kara.webp ├── Connor.webp ├── chloe.webp ├── logout.png ├── pattern.png ├── ai-avatar.png ├── covers │ ├── hcl.jpg │ ├── tcs.png │ ├── adobe.png │ ├── quora.png │ ├── skype.png │ ├── wipro.png │ ├── amazon.png │ ├── facebook.png │ ├── flipkart.png │ ├── google.png │ ├── infosys.jpg │ ├── reddit.png │ ├── spotify.png │ ├── telegram.png │ ├── accenture.png │ ├── microsoft.png │ └── pinterest.png ├── githubnav.png ├── hero.png.webp ├── profile1.webp ├── profile2.webp ├── profile3.webp ├── profile4.webp ├── profile5.webp ├── profile6.webp ├── Chloe RT600.webp ├── google-icon.png ├── user-avatar.png ├── window.svg ├── file.svg ├── profile.svg ├── tailwind.svg ├── globe.svg ├── star.svg ├── tech.svg ├── upload.svg ├── logo.svg ├── react.svg └── calendar.svg ├── postcss.config.mjs ├── lib ├── vapi.sdk.ts ├── utils.ts └── actions │ ├── auth.action.ts │ └── generate.action.ts ├── .txt ├── next.config.ts ├── components ├── Loading.tsx ├── SignOutButton.tsx ├── ui │ ├── sonner.tsx │ ├── label.tsx │ ├── input.tsx │ ├── button.tsx │ ├── form.tsx │ └── InfiniteCards.tsx ├── DisplayTechicons.tsx ├── StartInterviewButton.tsx ├── BackToDashboardButton.tsx ├── RetakeInterviewButton.tsx ├── CheckFeedbackInterviewButton.tsx ├── FormField.tsx ├── Loader.tsx ├── Testimonials.tsx ├── HomeLoginbtn.tsx ├── Alert.tsx ├── Footer.tsx ├── GetStartedbtn.tsx ├── InterviewCard.tsx ├── Features.tsx ├── Github.tsx ├── HomePlay.tsx ├── Agent.tsx ├── AuthForm.tsx └── GetInterview.tsx ├── eslint.config.mjs ├── components.json ├── constants ├── feature.ts ├── testimonials.ts └── index.ts ├── .gitignore ├── tsconfig.json ├── firebass ├── admin.ts └── client.ts ├── LICENSE ├── types ├── vapi.d.ts └── index.d.ts ├── package.json └── README.md /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/kara.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/kara.webp -------------------------------------------------------------------------------- /public/Connor.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/Connor.webp -------------------------------------------------------------------------------- /public/chloe.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/chloe.webp -------------------------------------------------------------------------------- /public/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/logout.png -------------------------------------------------------------------------------- /public/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/pattern.png -------------------------------------------------------------------------------- /public/ai-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/ai-avatar.png -------------------------------------------------------------------------------- /public/covers/hcl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/hcl.jpg -------------------------------------------------------------------------------- /public/covers/tcs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/tcs.png -------------------------------------------------------------------------------- /public/githubnav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/githubnav.png -------------------------------------------------------------------------------- /public/hero.png.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/hero.png.webp -------------------------------------------------------------------------------- /public/profile1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/profile1.webp -------------------------------------------------------------------------------- /public/profile2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/profile2.webp -------------------------------------------------------------------------------- /public/profile3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/profile3.webp -------------------------------------------------------------------------------- /public/profile4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/profile4.webp -------------------------------------------------------------------------------- /public/profile5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/profile5.webp -------------------------------------------------------------------------------- /public/profile6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/profile6.webp -------------------------------------------------------------------------------- /public/Chloe RT600.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/Chloe RT600.webp -------------------------------------------------------------------------------- /public/covers/adobe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/adobe.png -------------------------------------------------------------------------------- /public/covers/quora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/quora.png -------------------------------------------------------------------------------- /public/covers/skype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/skype.png -------------------------------------------------------------------------------- /public/covers/wipro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/wipro.png -------------------------------------------------------------------------------- /public/google-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/google-icon.png -------------------------------------------------------------------------------- /public/user-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/user-avatar.png -------------------------------------------------------------------------------- /public/covers/amazon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/amazon.png -------------------------------------------------------------------------------- /public/covers/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/facebook.png -------------------------------------------------------------------------------- /public/covers/flipkart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/flipkart.png -------------------------------------------------------------------------------- /public/covers/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/google.png -------------------------------------------------------------------------------- /public/covers/infosys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/infosys.jpg -------------------------------------------------------------------------------- /public/covers/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/reddit.png -------------------------------------------------------------------------------- /public/covers/spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/spotify.png -------------------------------------------------------------------------------- /public/covers/telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/telegram.png -------------------------------------------------------------------------------- /public/covers/accenture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/accenture.png -------------------------------------------------------------------------------- /public/covers/microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/microsoft.png -------------------------------------------------------------------------------- /public/covers/pinterest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbhishekGanvir/IntervueAI/HEAD/public/covers/pinterest.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /lib/vapi.sdk.ts: -------------------------------------------------------------------------------- 1 | import Vapi from '@vapi-ai/web'; 2 | 3 | export const vapi = new Vapi(process.env.NEXT_PUBLIC_VAPI_WEB_TOKEN!); -------------------------------------------------------------------------------- /app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import AuthForm from '@/components/AuthForm' 2 | import React from 'react' 3 | 4 | const page = () => { 5 | return 6 | } 7 | 8 | export default page -------------------------------------------------------------------------------- /app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import AuthForm from '@/components/AuthForm' 2 | import React from 'react' 3 | 4 | const page = () => { 5 | return 6 | } 7 | 8 | export default page -------------------------------------------------------------------------------- /.txt: -------------------------------------------------------------------------------- 1 | npx create-nextapp@ /. 2 | npx shadcn@latest init 3 | npm run dev 4 | npx shadcn@latest add button input sonner 5 | npm install dayjs 6 | npm install firebase 7 | npm install firebase-admin --save 8 | npm install ai @ai-sdk/google 9 | npm install @vapi-ai/web -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { NextConfig } from "next"; 3 | 4 | const nextConfig: NextConfig = { 5 | /* config options here */ 6 | eslint:{ 7 | ignoreDuringBuilds: true, 8 | }, 9 | typescript: { 10 | ignoreBuildErrors: true, 11 | }, 12 | compiler: { 13 | styledComponents: true, // Ensures styled-components work with SSR 14 | }, 15 | 16 | }; 17 | 18 | export default nextConfig; 19 | -------------------------------------------------------------------------------- /app/(dash)/interview/page.tsx: -------------------------------------------------------------------------------- 1 | import Agent from '@/components/Agent' 2 | import { getCurrentUser } from '@/lib/actions/auth.action' 3 | import React from 'react' 4 | 5 | const page = async () => { 6 | const user = await getCurrentUser(); 7 | return ( 8 | <> 9 |

Interview Generation

10 | 11 | 12 | ) 13 | } 14 | 15 | export default page -------------------------------------------------------------------------------- /components/Loading.tsx: -------------------------------------------------------------------------------- 1 | // app/loading.tsx 2 | import Loader from '@/components/Loader'; 3 | 4 | export default function Loading() { 5 | return ( 6 |
7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | 3 | import { isAuthenticated } from '@/lib/actions/auth.action' 4 | import { redirect } from 'next/navigation'; 5 | const Authlayout = async ({children} : {children: ReactNode}) => { 6 | const isUserAuthenticated = await isAuthenticated(); 7 | if (isUserAuthenticated) redirect('/dashboard'); 8 | return ( 9 |
{children}
10 | ) 11 | } 12 | export default Authlayout -------------------------------------------------------------------------------- /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 = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 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 | } -------------------------------------------------------------------------------- /components/SignOutButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { signOut } from '@/lib/actions/auth.action' 4 | import Image from 'next/image'; 5 | 6 | 7 | const SignOutButton = () => { 8 | const handleSignOut = () => { 9 | signOut() 10 | } 11 | 12 | return ( 13 | <> 14 | 18 | 19 | ); 20 | } 21 | 22 | export default SignOutButton 23 | -------------------------------------------------------------------------------- /constants/feature.ts: -------------------------------------------------------------------------------- 1 | export const features = [ 2 | { 3 | title: 'AI-Powered Mock Interviews', 4 | description: 'Experience realistic mock interviews tailored to your needs, powered by advanced AI technology.', 5 | 6 | }, 7 | { 8 | title: 'Real-Time Voice Integration', 9 | description: 'No forms. No typing. Just speak everything works seamlessly with your voice.', 10 | 11 | }, 12 | { 13 | title: 'Resume Analysis', 14 | description: 'Our AI analyzes your resume and provides a concise summary to highlight your strengths.', 15 | 16 | }, 17 | 18 | ] -------------------------------------------------------------------------------- /public/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /components/DisplayTechicons.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { getTechLogos } from '@/lib/utils'; 3 | import Image from 'next/image'; 4 | import React from 'react' 5 | 6 | const DisplayTechicons = async ({techStack}: TechIconProps) => { 7 | const techIcons = await getTechLogos(techStack) 8 | return ( 9 |
{techIcons.slice(0,3).map (({tech, url}, index)=> 10 | 11 |
12 | {tech} 13 | {tech} 14 |
15 | )}
16 | ) 17 | } 18 | 19 | export default DisplayTechicons; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /components/StartInterviewButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { Button } from "./ui/button" 5 | import Loading from "./Loading" 6 | import { useRouter } from "next/navigation" 7 | 8 | const StartInterviewButton = () => { 9 | const [loading, setLoading] = useState(false) 10 | const router = useRouter() 11 | 12 | const handleClick = () => { 13 | setLoading(true) 14 | router.push("/interview") 15 | } 16 | 17 | return ( 18 | 22 | ) 23 | } 24 | 25 | export default StartInterviewButton 26 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /components/BackToDashboardButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { Button } from "@/components/ui/button" 6 | import Loading from "@/components/Loading" 7 | 8 | const BackToDashboardButton = () => { 9 | const [loading, setLoading] = useState(false) 10 | const router = useRouter() 11 | 12 | const handleClick = () => { 13 | setLoading(true) 14 | router.push("/dashboard") 15 | } 16 | 17 | return ( 18 | 24 | ) 25 | } 26 | 27 | export default BackToDashboardButton 28 | -------------------------------------------------------------------------------- /firebass/admin.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp, getApps, cert } from "firebase-admin/app"; 2 | import { getAuth } from "firebase-admin/auth"; 3 | import { getFirestore } from "firebase-admin/firestore"; 4 | 5 | // Initialize Firebase Admin SDK 6 | function initFirebaseAdmin() { 7 | const apps = getApps(); 8 | 9 | if (!apps.length) { 10 | initializeApp({ 11 | credential: cert({ 12 | projectId: process.env.FIREBASE_PROJECT_ID, 13 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL, 14 | // Replace newlines in the private key 15 | privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"), 16 | }), 17 | }); 18 | } 19 | 20 | return { 21 | auth: getAuth(), 22 | db: getFirestore(), 23 | }; 24 | } 25 | 26 | export const { auth, db } = initFirebaseAdmin(); -------------------------------------------------------------------------------- /components/RetakeInterviewButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { Button } from "@/components/ui/button" 6 | import Loading from "@/components/Loading" 7 | 8 | interface Props { 9 | interviewId: string 10 | } 11 | 12 | const RetakeInterviewButton = ({ interviewId }: Props) => { 13 | const [loading, setLoading] = useState(false) 14 | const router = useRouter() 15 | 16 | const handleClick = () => { 17 | setLoading(true) 18 | router.push(`/interview/${interviewId}`) 19 | } 20 | 21 | return ( 22 | 28 | ) 29 | } 30 | 31 | export default RetakeInterviewButton 32 | -------------------------------------------------------------------------------- /public/tailwind.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Mona_Sans } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Toaster } from "sonner"; 5 | 6 | const monaSans = Mona_Sans({ 7 | variable: "--font-mona-sans", 8 | subsets: ["latin"], 9 | //display: "swap", // ⬅️ improves font performance 10 | }); 11 | 12 | 13 | 14 | export const metadata: Metadata = { 15 | title: "IntervueAI - Precision in Recruitment", 16 | description: "IntervueAI is an AI-driven interview platform with real-time voice integration and automated feedback, helping users prepare faster and smarter.", 17 | }; 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: Readonly<{ 22 | children: React.ReactNode; 23 | }>) { 24 | return ( 25 | 26 | 29 | {children} 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /components/CheckFeedbackInterviewButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { Button } from "./ui/button" 6 | import Loading from "./Loading" 7 | 8 | interface Props { 9 | hasFeedback: boolean 10 | interviewId: string 11 | } 12 | 13 | const CheckFeedbackInterviewButton = ({ hasFeedback, interviewId }: Props) => { 14 | const [loading, setLoading] = useState(false) 15 | const router = useRouter() 16 | 17 | const handleClick = () => { 18 | setLoading(true) 19 | const url = hasFeedback 20 | ? `/interview/${interviewId}/feedback` 21 | : `/interview/${interviewId}` 22 | router.push(url) 23 | } 24 | 25 | return ( 26 | 30 | ) 31 | } 32 | 33 | export default CheckFeedbackInterviewButton 34 | -------------------------------------------------------------------------------- /components/FormField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; 3 | import {Input} from "@/components/ui/input"; 4 | import { Controller, FieldValues } from "react-hook-form"; 5 | 6 | interface FormFieldProps { 7 | control: Control; 8 | name: Path; 9 | label: string; 10 | placeholder?: string; 11 | type?:'text' | 'email' | 'password' | 'file' 12 | } 13 | 14 | const FormField = ({control,name,label,placeholder, type="text"}: FormFieldProps) => ( 15 | ( 19 | 20 | {label} 21 | 22 | 23 | 24 | 25 | 26 | )} 27 | /> 28 | ) 29 | 30 | export default FormField -------------------------------------------------------------------------------- /app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { isAuthenticated } from '@/lib/actions/auth.action' 3 | import { redirect } from 'next/navigation'; 4 | import Link from 'next/link'; 5 | import Image from 'next/image'; 6 | import HomeLoginbtn from '@/components/HomeLoginbtn'; 7 | const Rootlayout = async ({children} : {children: ReactNode}) => { 8 | const isUserAuthenticated = await isAuthenticated(); 9 | if (isUserAuthenticated) redirect('/dashboard'); 10 | return ( 11 |
12 | 21 | {children} 22 |
23 | ) 24 | } 25 | 26 | export default Rootlayout -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Loader = () => { 5 | return ( 6 | 7 |
8 |
9 |
10 | 11 | ); 12 | } 13 | 14 | const StyledWrapper = styled.div` 15 | .spinner { 16 | background-image: linear-gradient(rgb(186, 66, 255) 35%,rgb(0, 225, 255)); 17 | width: 100px; 18 | height: 100px; 19 | animation: spinning82341 1.7s linear infinite; 20 | text-align: center; 21 | border-radius: 50px; 22 | filter: blur(1px); 23 | box-shadow: 0px -5px 20px 0px rgb(186, 66, 255), 0px 5px 20px 0px rgb(0, 225, 255); 24 | } 25 | 26 | .spinner1 { 27 | background-color: rgb(36, 36, 36); 28 | width: 100px; 29 | height: 100px; 30 | border-radius: 50px; 31 | filter: blur(10px); 32 | } 33 | 34 | @keyframes spinning82341 { 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | }`; 39 | 40 | export default Loader; 41 | -------------------------------------------------------------------------------- /app/api/google-auth/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/google-auth/route.ts 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { db } from "@/firebass/admin"; 4 | import { setSessionCookie } from "@/lib/actions/auth.action"; 5 | 6 | export async function POST(req: NextRequest) { 7 | try { 8 | const { uid, name, email, idToken } = await req.json(); 9 | 10 | // Check if user exists in Firestore 11 | const userRef = db.collection("users").doc(uid); 12 | const userDoc = await userRef.get(); 13 | 14 | if (!userDoc.exists) { 15 | // Save to Firestore if new user 16 | await userRef.set({ 17 | name, 18 | email, 19 | }); 20 | } 21 | 22 | // Set session cookie 23 | await setSessionCookie(idToken); 24 | 25 | return NextResponse.json({ success: true, message: "User signed in successfully." }); 26 | } catch (error) { 27 | console.error("Google Auth error:", error); 28 | return NextResponse.json({ success: false, message: "Something went wrong." }, { status: 500 }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/(dash)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import React, { ReactNode } from 'react' 4 | import { isAuthenticated } from '@/lib/actions/auth.action' 5 | import { redirect } from 'next/navigation'; 6 | import SignOutButton from '@/components/SignOutButton'; 7 | import Card from '@/components/Alert'; 8 | const Rootlayout = async ({children} : {children: ReactNode}) => { 9 | const isUserAuthenticated = await isAuthenticated(); 10 | if (!isUserAuthenticated) redirect('/'); 11 | return ( 12 |
13 | 20 | {/* Show the notice once per login */} 21 | 22 | {children} 23 |
24 | ) 25 | } 26 | 27 | export default Rootlayout -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Abhishek Ganvir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/Testimonials.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { testimonials } from "@/constants/testimonials"; 6 | import { InfiniteMovingCards } from "@/components/ui/InfiniteCards"; 7 | 8 | 9 | const Testimonials = () => { 10 | return ( 11 |
12 |

13 | Kind words from 14 | satisfied Users 15 |

16 | 17 |
18 |
22 | 27 |
28 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export default Testimonials -------------------------------------------------------------------------------- /types/vapi.d.ts: -------------------------------------------------------------------------------- 1 | enum MessageTypeEnum { 2 | TRANSCRIPT = "transcript", 3 | FUNCTION_CALL = "function-call", 4 | FUNCTION_CALL_RESULT = "function-call-result", 5 | ADD_MESSAGE = "add-message", 6 | } 7 | 8 | enum MessageRoleEnum { 9 | USER = "user", 10 | SYSTEM = "system", 11 | ASSISTANT = "assistant", 12 | } 13 | 14 | enum TranscriptMessageTypeEnum { 15 | PARTIAL = "partial", 16 | FINAL = "final", 17 | } 18 | 19 | interface BaseMessage { 20 | type: MessageTypeEnum; 21 | } 22 | 23 | interface TranscriptMessage extends BaseMessage { 24 | type: MessageTypeEnum.TRANSCRIPT; 25 | role: MessageRoleEnum; 26 | transcriptType: TranscriptMessageTypeEnum; 27 | transcript: string; 28 | } 29 | 30 | interface FunctionCallMessage extends BaseMessage { 31 | type: MessageTypeEnum.FUNCTION_CALL; 32 | functionCall: { 33 | name: string; 34 | parameters: unknown; 35 | }; 36 | } 37 | 38 | interface FunctionCallResultMessage extends BaseMessage { 39 | type: MessageTypeEnum.FUNCTION_CALL_RESULT; 40 | functionCallResult: { 41 | forwardToClientEnabled?: boolean; 42 | result: unknown; 43 | [a: string]: unknown; 44 | }; 45 | } 46 | 47 | type Message = 48 | | TranscriptMessage 49 | | FunctionCallMessage 50 | | FunctionCallResultMessage; 51 | -------------------------------------------------------------------------------- /public/tech.svg: -------------------------------------------------------------------------------- 1 | tech -------------------------------------------------------------------------------- /components/HomeLoginbtn.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React from 'react' 3 | 4 | import styled from 'styled-components'; 5 | const HomeLoginbtn = () => { 6 | return ( 7 | 8 | 12 | 13 | ); 14 | } 15 | 16 | const StyledWrapper = styled.div` 17 | button { 18 | position: relative; 19 | padding: 4px 22px; 20 | background: transparent; 21 | font-size: 17px; 22 | font-weight: 500; 23 | color:#DDDFFF ; 24 | border: 2px solid #6F75B3; 25 | border-radius: 8px; 26 | box-shadow: 0 0 1em .25em var(--glow-color), 27 | 0 0 4em 1em var(--glow-spread-color), 28 | inset 0 0 .75em .25em var(--glow-color); 29 | text-shadow: 0 0 .5em var(--glow-color);; 30 | transition: all 0.3s ease-in-out; 31 | cursor: pointer; 32 | } 33 | 34 | 35 | 36 | button:hover { 37 | background: transparent; 38 | color: #A998DA; 39 | 40 | box-shadow:0 0 1em .25em var(--glow-color), 41 | 0 0 4em 2em var(--glow-spread-color), 42 | inset 0 0 .75em .25em var(--glow-color); ; 43 | } 44 | 45 | 46 | .fil0 { 47 | fill: 0 0 1em .25em var(--glow-color), 48 | 0 0 4em 2em var(--glow-spread-color), 49 | inset 0 0 .75em .25em var(--glow-color);; 50 | }`; 51 | 52 | 53 | 54 | export default HomeLoginbtn -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intervue_ai", 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 | }, 11 | "dependencies": { 12 | "@ai-sdk/google": "^1.2.10", 13 | "@hookform/resolvers": "^4.1.3", 14 | "@radix-ui/react-label": "^2.1.2", 15 | "@radix-ui/react-slot": "^1.1.2", 16 | "@vapi-ai/web": "^2.2.5", 17 | "ai": "^4.3.4", 18 | "class-variance-authority": "^0.7.1", 19 | "clsx": "^2.1.1", 20 | "dayjs": "^1.11.13", 21 | "firebase": "^11.6.0", 22 | "firebase-admin": "^13.2.0", 23 | "lucide-react": "^0.485.0", 24 | "next": "15.2.4", 25 | "next-pwa": "^5.6.0", 26 | "next-themes": "^0.4.6", 27 | "react": "^19.0.0", 28 | "react-dom": "^19.0.0", 29 | "react-hook-form": "^7.55.0", 30 | "sonner": "^2.0.2", 31 | "styled-components": "^6.1.17", 32 | "tailwind-merge": "^3.0.2", 33 | "tailwindcss-animate": "^1.0.7", 34 | "tw-animate-css": "^1.2.5", 35 | "zod": "^3.24.2" 36 | }, 37 | "devDependencies": { 38 | "@eslint/eslintrc": "^3", 39 | "@tailwindcss/postcss": "^4", 40 | "@types/next-pwa": "^5.6.9", 41 | "@types/node": "^20", 42 | "@types/react": "^19", 43 | "@types/react-dom": "^19", 44 | "eslint": "^9", 45 | "eslint-config-next": "15.2.4", 46 | "tailwindcss": "^4", 47 | "typescript": "^5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { interviewCovers, mappings } from "@/constants"; 2 | import { clsx, type ClassValue } from "clsx"; 3 | import { twMerge } from "tailwind-merge"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | 9 | const techIconBaseURL = "https://cdn.jsdelivr.net/gh/devicons/devicon/icons"; 10 | 11 | const normalizeTechName = (tech: string) => { 12 | const key = tech.toLowerCase().replace(/\.js$/, "").replace(/\s+/g, ""); 13 | return mappings[key as keyof typeof mappings]; 14 | }; 15 | 16 | const checkIconExists = async (url: string) => { 17 | try { 18 | const response = await fetch(url, { method: "HEAD" }); 19 | return response.ok; // Returns true if the icon exists 20 | } catch { 21 | return false; 22 | } 23 | }; 24 | 25 | export const getTechLogos = async (techArray: string[]) => { 26 | const logoURLs = techArray.map((tech) => { 27 | const normalized = normalizeTechName(tech); 28 | return { 29 | tech, 30 | url: `${techIconBaseURL}/${normalized}/${normalized}-original.svg`, 31 | }; 32 | }); 33 | 34 | const results = await Promise.all( 35 | logoURLs.map(async ({ tech, url }) => ({ 36 | tech, 37 | url: (await checkIconExists(url)) ? url : "/tech.svg", 38 | })) 39 | ); 40 | 41 | return results; 42 | }; 43 | 44 | export const getRandomInterviewCover = () => { 45 | const randomIndex = Math.floor(Math.random() * interviewCovers.length); 46 | return `/covers${interviewCovers[randomIndex]}`; 47 | }; 48 | -------------------------------------------------------------------------------- /app/(dash)/interview/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import GetInterview from "@/components/GetInterview" 3 | import DisplayTechicons from "@/components/DisplayTechicons"; 4 | import { getCurrentUser } from "@/lib/actions/auth.action"; 5 | import { getInterviewById } from "@/lib/actions/generate.action"; 6 | import { getRandomInterviewCover } from "@/lib/utils"; 7 | import Image from "next/image"; 8 | import { redirect } from "next/navigation"; 9 | 10 | const page = async ({ params }: RouteParams) => { 11 | const {id} = await params; 12 | const user = await getCurrentUser(); 13 | const interview = await getInterviewById(id); 14 | if(!interview) redirect('/dashboard') 15 | return ( 16 | <> 17 |
18 |
19 |
20 | cover-image 21 |

{interview.role}

22 |
23 | 24 |
25 | 26 |
27 | 28 |

{interview.type}

29 | 30 |
31 |
32 | 33 |
34 | 35 | 42 | 43 | ) 44 | } 45 | 46 | export default page -------------------------------------------------------------------------------- /components/Alert.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useEffect, useState } from 'react'; 3 | 4 | const Card = () => { 5 | const [show, setShow] = useState(false); 6 | 7 | useEffect(() => { 8 | const seen = localStorage.getItem('hasSeenNotice'); 9 | if (!seen) { 10 | setShow(true); 11 | localStorage.setItem('hasSeenNotice', 'true'); 12 | } 13 | }, []); 14 | 15 | if (!show) return null; 16 | 17 | return ( 18 |
19 |
20 | 21 | {/* Decorative stripe on the left */} 22 |
23 | 24 | {/* Close button */} 25 | 33 | 34 | {/* Content */} 35 |

Notice!

36 |

37 | Due to the discontinuation of free plans by Vapi for AI voice integration, we are currently unable to provide this service. However, we're working on a comeback. 38 |
39 | Stay tuned 🔔 40 |

41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Card; 47 | -------------------------------------------------------------------------------- /constants/testimonials.ts: -------------------------------------------------------------------------------- 1 | export const testimonials = [ 2 | { 3 | quote: 4 | "Using IntervueAI was a game-changer for my job prep. The way abhi built the platform—especially the real-time voice feedback—felt like I was talking to a real interviewer. Super intuitive and helpful!", 5 | name: "Anjali Mehra", 6 | title: "Final Year Student, BITS Pilani", 7 | }, 8 | { 9 | quote: 10 | "I was blown away by IntervueAI’s accuracy in evaluating my speech and giving relevant suggestions. Hats off to abhi for building something so futuristic yet user-friendly.", 11 | name: "Rohan Deshmukh", 12 | title: "Software Engineer Intern, Infosys", 13 | }, 14 | { 15 | quote: 16 | "As someone who struggles with interview anxiety, IntervueAI gave me the confidence to face real interviews. The conversational agent feels human, and abhi’s work behind this is simply brilliant.", 17 | name: "Priya Nair", 18 | title: "Graduate Trainee, Wipro", 19 | }, 20 | { 21 | quote: 22 | "Abhi’s IntervueAI is not just a mock interview tool—it's an experience. From voice transcription to feedback, everything felt polished and thoughtful. Really impressive work!", 23 | name: "Arjun Sethi", 24 | title: "HR Associate, TCS", 25 | }, 26 | { 27 | quote: 28 | "What impressed me the most was how natural and intelligent the AI felt. It's clear that abhi understands both tech and the pain points of interviewees. Kudos for solving a real problem!", 29 | name: "Neha Kapoor", 30 | title: "Talent Acquisition Manager, Zoho", 31 | }, 32 | { 33 | quote: 34 | "IntervueAI helped my students gain confidence before placement season. abhi’s dedication to tech with purpose is truly inspiring. Looking forward to seeing what he builds next.", 35 | name: "Dr. Rajeev Rangan", 36 | title: "Placement Officer, VJTI Mumbai", 37 | }, 38 | ]; 39 | 40 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | import Github from './Github'; 5 | // Remove if not using react-router 6 | import { useEffect, useState } from 'react'; 7 | const Footer = () => { 8 | const [year, setYear] = useState(null); 9 | 10 | useEffect(() => { 11 | setYear(new Date().getFullYear()); 12 | }, []); 13 | return ( 14 |
15 | {/* Curved border effect */} 16 | 17 | 18 | {/* Logo and tagline */} 19 |
20 | Logo 21 | 22 | < Github /> 23 | 24 |

25 | Your Gateway to Smarter Interview Preparation! 26 |

27 |
28 | 29 | 30 | {/* Divider */} 31 |
32 | 33 | {/* Bottom row */} 34 |
35 |
36 | LinkedIn 37 | GitHub 38 | Contact Us 39 |
40 |
41 | {year && <>Copyright © {year} | IntervueAI} 42 |
43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Footer; 49 | -------------------------------------------------------------------------------- /public/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /components/GetStartedbtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components'; 3 | const GetStartedbtn = () => { 4 | return ( 5 | 6 | 8 | 9 | ); 10 | } 11 | 12 | const StyledWrapper = styled.div` 13 | button { 14 | --glow-color: rgb(217, 176, 255); 15 | --glow-spread-color: rgba(191, 123, 255, 0.781); 16 | --enhanced-glow-color: rgb(231, 206, 255); 17 | --btn-color: rgb(100, 61, 136); 18 | border: .25em solid var(--glow-color); 19 | padding: 1em 3em; 20 | color: var(--glow-color); 21 | font-size: 15px; 22 | font-weight: bold; 23 | background-color: var(--btn-color); 24 | border-radius: 1em; 25 | outline: none; 26 | box-shadow: 0 0 1em .25em var(--glow-color), 27 | 0 0 4em 1em var(--glow-spread-color), 28 | inset 0 0 .75em .25em var(--glow-color); 29 | text-shadow: 0 0 .5em var(--glow-color); 30 | position: relative; 31 | transition: all 0.3s; 32 | } 33 | 34 | button::after { 35 | pointer-events: none; 36 | content: ""; 37 | position: absolute; 38 | top: 120%; 39 | left: 0; 40 | height: 100%; 41 | width: 100%; 42 | background-color: var(--glow-spread-color); 43 | filter: blur(2em); 44 | opacity: .7; 45 | transform: perspective(1.5em) rotateX(35deg) scale(1, .6); 46 | } 47 | 48 | button:hover { 49 | color: var(--btn-color); 50 | background-color: var(--glow-color); 51 | box-shadow: 0 0 1em .25em var(--glow-color), 52 | 0 0 4em 2em var(--glow-spread-color), 53 | inset 0 0 .75em .25em var(--glow-color); 54 | } 55 | 56 | button:active { 57 | box-shadow: 0 0 0.6em .25em var(--glow-color), 58 | 0 0 2.5em 2em var(--glow-spread-color), 59 | inset 0 0 .5em .25em var(--glow-color); 60 | }`; 61 | 62 | export default GetStartedbtn -------------------------------------------------------------------------------- /firebass/client.ts: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp, getApp, getApps } from "firebase/app"; 3 | import { getAuth,GoogleAuthProvider, signInWithPopup } from "firebase/auth"; 4 | import { getFirestore } from "firebase/firestore"; 5 | 6 | 7 | 8 | // TODO: Add SDKs for Firebase products that you want to use 9 | // https://firebase.google.com/docs/web/setup#available-libraries 10 | 11 | // Your web app's Firebase configuration 12 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional 13 | 14 | const firebaseConfig = { 15 | apiKey: "AIzaSyDA2BCiIPeAlhmC7XKcPEVl6KtLL_QNtp0", 16 | authDomain: "intervueai-7d261.firebaseapp.com", 17 | projectId: "intervueai-7d261", 18 | storageBucket: "intervueai-7d261.firebasestorage.app", 19 | messagingSenderId: "29325578515", 20 | appId: "1:29325578515:web:0247e625c5e25b17c0cae6", 21 | measurementId: "G-STGDZHYXGF" 22 | }; 23 | 24 | // Initialize Firebase 25 | const app = !getApps.length ? initializeApp(firebaseConfig) : getApp(); 26 | 27 | export const provider = new GoogleAuthProvider(); 28 | export const auth = getAuth(app); 29 | auth.languageCode = 'en'; 30 | export const db = getFirestore(app); 31 | 32 | export const signInWithGoogle = async () => { 33 | try { 34 | const result = await signInWithPopup(auth, provider); 35 | const credential = GoogleAuthProvider.credentialFromResult(result); 36 | const token = credential?.accessToken; 37 | const user = result.user; 38 | 39 | const idToken = await user.getIdToken(); 40 | 41 | // Send to server to store in DB and set session 42 | await fetch("/api/google-auth", { 43 | method: "POST", 44 | headers: { 45 | "Content-Type": "application/json" 46 | }, 47 | body: JSON.stringify({ 48 | uid: user.uid, 49 | name: user.displayName, 50 | email: user.email, 51 | idToken, 52 | }), 53 | }); 54 | 55 | return { user, token, idToken }; // ✅ returning all 56 | } catch (error) { 57 | console.error("Error signing in with Google: ", error); 58 | throw error; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /app/api/google-auth/vapi/generate/route.ts: -------------------------------------------------------------------------------- 1 | import { generateText } from "ai"; 2 | import { google } from "@ai-sdk/google"; 3 | 4 | 5 | import { getRandomInterviewCover } from "@/lib/utils"; 6 | import { db } from "@/firebass/admin"; 7 | 8 | export async function GET() { 9 | return Response.json({ success: true, data: 'THANK YOU!'}, {status:200}); 10 | } 11 | 12 | export async function POST(request: Request){ 13 | const { type, role , level, techstack, amount, profile, userid } = await request.json(); 14 | 15 | try { 16 | const { text: questions } = await generateText({ 17 | model: google("gemini-2.0-flash-001"), 18 | prompt: `Prepare questions for a job interview. 19 | The job role is ${role}. 20 | The job experience level is ${level}. 21 | The tech stack used in the job is: ${techstack}. 22 | The focus between behavioural and technical questions should lean towards: ${type}. 23 | The user's cv/resume profile is: ${profile}. 24 | The amount of questions required is: ${amount}. 25 | Please return only the questions, without any additional text. 26 | The questions are going to be read by a voice assistant so do not use "/" or "*" or any other special characters which might break the voice assistant. 27 | Return the questions formatted like this: 28 | ["Question 1", "Question 2", "Question 3"] 29 | 30 | Thank you! <3 31 | `, 32 | }); 33 | const interview = { 34 | role: role, 35 | type: type, 36 | level: level, 37 | profile: profile, 38 | techstack: techstack.split(","), 39 | questions: JSON.parse(questions), 40 | userId: userid, 41 | finalized: true, 42 | coverImage: getRandomInterviewCover(), 43 | createdAt: new Date().toISOString(), 44 | }; 45 | 46 | await db.collection("interviews").add(interview); 47 | 48 | return Response.json({success: true}, {status:200}) 49 | 50 | } catch (error) { 51 | console.error(error); 52 | } 53 | } -------------------------------------------------------------------------------- /components/InterviewCard.tsx: -------------------------------------------------------------------------------- 1 | import { getRandomInterviewCover } from "@/lib/utils"; 2 | import dayjs from "dayjs"; 3 | import Image from "next/image"; 4 | 5 | import DisplayTechicons from "./DisplayTechicons"; 6 | import { getFeedbackByInterviewId } from "@/lib/actions/generate.action"; 7 | import CheckFeedbackInterviewButton from "./CheckFeedbackInterviewButton"; 8 | 9 | const InterviewCard = async ({ id, userId, role, type, techstack, createdAt}: InterviewCardProps) => { 10 | const feedback = userId && id ? await getFeedbackByInterviewId({interviewId : id, userId}) : null; 11 | const normalizedType = /mix/gi.test(type) ? 'Mixed' : type; 12 | 13 | const formattedDate = dayjs(feedback?.createdAt || createdAt || Date.now()).format('MMM D, YYYY'); 14 | return ( 15 |
16 | 17 |
18 | 19 |
20 | 21 |

22 | {normalizedType}

23 |
24 | 25 | cover 26 | 27 |

{role} Interview

28 | 29 |
30 | 31 |
32 | 33 | calender 34 |

{formattedDate}

35 | 36 |
star 37 |

{feedback?.totalScore || '---'}/100

38 |
39 |
40 |
41 |

{feedback?.finalAssessment || "You haven't taken interview yet. Take it now to improve your skills." }

42 | 43 |
44 | 45 | 46 | 47 |
48 |
49 |
50 | 51 | ) 52 | } 53 | 54 | export default InterviewCard; -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } 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-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Feedback { 2 | id: string; 3 | interviewId: string; 4 | totalScore: number; 5 | categoryScores: Array<{ 6 | name: string; 7 | score: number; 8 | comment: string; 9 | }>; 10 | strengths: string[]; 11 | areasForImprovement: string[]; 12 | finalAssessment: string; 13 | createdAt: string; 14 | } 15 | 16 | interface Interview { 17 | id: string; 18 | role: string; 19 | level: string; 20 | questions: string[]; 21 | techstack: string[]; 22 | createdAt: string; 23 | userId: string; 24 | type: string; 25 | finalized: boolean; 26 | } 27 | 28 | interface CreateFeedbackParams { 29 | interviewId: string; 30 | userId: string; 31 | transcript: { role: string; content: string }[]; 32 | feedbackId?: string; 33 | } 34 | 35 | interface User { 36 | name: string; 37 | email: string; 38 | id: string; 39 | } 40 | 41 | interface InterviewCardProps { 42 | id?: string; 43 | userId?: string; 44 | role: string; 45 | type: string; 46 | techstack: string[]; 47 | createdAt?: string; 48 | } 49 | 50 | interface AgentProps { 51 | userName: string; 52 | userId?: string; 53 | interviewId?: string; 54 | feedbackId?: string; 55 | type: "generate" | "interview"; 56 | questions?: string[]; 57 | } 58 | 59 | interface GetInterviewProps { 60 | userName: string; 61 | userId?: string; 62 | interviewId?: string; 63 | feedbackId?: string; 64 | type: "generate" | "interview"; 65 | questions?: string[]; 66 | } 67 | 68 | interface RouteParams { 69 | params: Promise>; 70 | searchParams: Promise>; 71 | } 72 | 73 | interface GetFeedbackByInterviewIdParams { 74 | interviewId: string; 75 | userId: string; 76 | } 77 | 78 | interface GetLatestInterviewsParams { 79 | userId: string; 80 | limit?: number; 81 | } 82 | 83 | interface SignInParams { 84 | email: string; 85 | idToken: string; 86 | } 87 | 88 | interface SignUpParams { 89 | uid: string; 90 | name: string; 91 | email: string; 92 | password: string; 93 | } 94 | 95 | type FormType = "sign-in" | "sign-up"; 96 | 97 | interface InterviewFormProps { 98 | interviewId: string; 99 | role: string; 100 | level: string; 101 | type: string; 102 | techstack: string[]; 103 | amount: number; 104 | } 105 | 106 | interface TechIconProps { 107 | techStack: string[]; 108 | } 109 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/(dash)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import InterviewCard from "@/components/InterviewCard" 3 | import StartInterviewButton from "@/components/StartInterviewButton" 4 | import { Button } from "@/components/ui/button" 5 | import {getCurrentUser} from "@/lib/actions/auth.action" 6 | import { getInterviewByUserId, getLatestInterviews } from "@/lib/actions/generate.action" 7 | import Image from "next/image" 8 | 9 | 10 | const page = async () => { 11 | const user = await getCurrentUser(); 12 | const [ userInterviews,latestInterviews ] = await Promise.all([ 13 | await getInterviewByUserId(user?.id!), 14 | await getLatestInterviews({userId: user?.id! }) 15 | ]) 16 | 17 | const hasPastInterviews = userInterviews?.length > 0; 18 | const hasUpcomingInterviews = latestInterviews?.length > 0; 19 | 20 | return ( 21 | <> 22 |
23 |
24 | 25 |
Get Interview-Ready with Smart Practice and Real Feedback
26 | 27 |

Because every word counts when the job’s on the line

28 | Kara 29 | 31 | 32 |
33 | 34 | Kara 35 |
36 |
37 |

Your Interviews

38 |
39 |
{ 40 | hasPastInterviews ? ( 41 | userInterviews?.map((interview) => ( 42 |
43 |
44 | ))) : (

You haven’t taken any Interviews yet

) } 45 | {/**/} 46 |
47 |
48 |
49 |
50 |

Take an Interview

51 |
52 |
53 | { 54 | hasUpcomingInterviews ? ( 55 | latestInterviews?.map((interview) => ( 56 |
57 |
58 | ))) : (

There are no interviews available

) } 59 | {/**/} 60 |
61 |
62 |
63 | 64 | ) 65 | } 66 | 67 | export default page -------------------------------------------------------------------------------- /app/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | // app/page.tsx 2 | "use client" 3 | import Features from '@/components/Features'; 4 | import Footer from '@/components/Footer'; 5 | import GetStartedbtn from '@/components/GetStartedbtn'; 6 | import HomePlay from '@/components/HomePlay'; 7 | import Testimonials from '@/components/Testimonials'; 8 | 9 | //import { isAuthenticated } from '@/lib/actions/auth.action' 10 | import Link from 'next/link' 11 | //import { redirect } from 'next/navigation' 12 | import React, { useEffect, useState } from 'react'; 13 | 14 | export default function LandingPage() { 15 | const [mounted, setMounted] = useState(false); 16 | 17 | useEffect(() => { 18 | setMounted(true); 19 | }, []); 20 | 21 | if (!mounted) return null; 22 | return ( 23 | <> 24 | <> 25 |
26 |
27 |
Where everyone 28 |
29 | suffers together
30 |
31 |

We know how brutal interviews can be. They don’t have to be.

32 |

Generate personalized mock interviews, watch how others handled theirs,

33 |

and get feedback that actually helps. 34 |

35 |
36 |
37 |
38 |
39 |
40 | 41 | < GetStartedbtn /> 42 | 43 |
44 |
45 | 46 |
47 | < HomePlay /> 48 |
49 | 50 | 51 | {/* This wrapper shouldn't limit full-screen sections */} 52 |
53 | 54 | {/* Normal content (centered) */} 55 |
56 | {/* ...hero text */} 57 |
WHY CHOOSE US
58 |
Unleash Your Potential with AI
59 |
Take your interview preparation to the next level with features designed for success.
60 |
61 | 62 | {/* Full-screen wide section */} 63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 | 71 |
72 | 73 |
74 |
75 |
76 | 77 | 78 | 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /public/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/actions/auth.action.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import { auth, db } from "@/firebass/admin"; 3 | import { cookies } from "next/headers"; 4 | import { redirect } from "next/navigation"; 5 | const TWO_WEEKS = 60 * 60 * 24 * 14; 6 | 7 | export async function signUp(params: SignUpParams){ 8 | const { uid, name, email } = params; 9 | try{ 10 | const userRecord = await db.collection('users').doc(uid).get(); 11 | if (userRecord.exists){ 12 | return { 13 | success: false, 14 | message: 'User already exists. Please sign in Instead' 15 | } 16 | } 17 | await db.collection('users').doc(uid).set({name, email}); 18 | return { 19 | success: true, 20 | message: 'User created successfully', 21 | } 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | } catch (e: any) { 24 | console.error('Error creating a user', e); 25 | if (e.code === 'auth/email-already-in-use'){ 26 | return { success: false, message: 'Email already in use' }; 27 | } 28 | 29 | return { 30 | success: false, 31 | message: 'Failed to create an account', 32 | } 33 | } 34 | } 35 | 36 | export async function signIn(params: SignInParams){ 37 | const { email, idToken} = params; 38 | try { 39 | const userRecord = await auth.getUserByEmail(email); 40 | if(!userRecord){ 41 | return{ 42 | success: false, 43 | message: 'User does not exist. Create an account instead.' 44 | } 45 | } 46 | await setSessionCookie(idToken); 47 | } catch (e) { 48 | console.log(e); 49 | } 50 | } 51 | 52 | export async function setSessionCookie(idToken: string){ 53 | const cookieStore = await cookies(); 54 | 55 | const sessionCookie = await auth.createSessionCookie(idToken, {expiresIn: TWO_WEEKS * 1000 }); 56 | 57 | cookieStore.set('session', sessionCookie,{ 58 | maxAge: TWO_WEEKS, 59 | httpOnly: true, 60 | secure: process.env.NODE_ENV === 'production', 61 | path:'/', 62 | sameSite:'lax', 63 | }) 64 | } 65 | 66 | export async function getCurrentUser(): Promise { 67 | const cookieStore = await cookies(); 68 | const sessionCookie = cookieStore.get("session")?.value; 69 | 70 | if(!sessionCookie) return null; 71 | try { 72 | const decodedClaims = await auth.verifySessionCookie(sessionCookie, true); 73 | 74 | const userRecord = await db. collection('users') 75 | .doc(decodedClaims.uid) 76 | .get(); 77 | if(!userRecord.exists)return null; 78 | return { 79 | ...userRecord.data(), 80 | id: userRecord.id, 81 | } as User; 82 | } catch (e) { 83 | console.log('Error verifying session cookie', e); 84 | return null; 85 | } 86 | } 87 | 88 | export async function isAuthenticated(){ 89 | const user = await getCurrentUser(); 90 | 91 | return !!user; 92 | } 93 | 94 | // Sign out user by clearing the session cookie 95 | export async function signOut() { 96 | const cookieStore = await cookies(); 97 | 98 | cookieStore.delete("session"); 99 | 100 | } 101 | 102 | 103 | -------------------------------------------------------------------------------- /app/(dash)/interview/[id]/feedback/page.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { redirect } from "next/navigation"; 5 | 6 | import { 7 | getFeedbackByInterviewId, 8 | getInterviewById, 9 | } from "@/lib/actions/generate.action" 10 | import { Button } from "@/components/ui/button"; 11 | import { getCurrentUser } from "@/lib/actions/auth.action"; 12 | import BackToDashboardButton from "@/components/BackToDashboardButton"; 13 | import RetakeInterviewButton from "@/components/RetakeInterviewButton"; 14 | 15 | const Feedback = async ({ params }: RouteParams) => { 16 | const { id } = await params; 17 | const user = await getCurrentUser(); 18 | 19 | const interview = await getInterviewById(id); 20 | if (!interview) redirect("/"); 21 | 22 | const feedback = await getFeedbackByInterviewId({ 23 | interviewId: id, 24 | userId: user?.id!, 25 | }); 26 | 27 | return ( 28 |
29 |
30 |

31 | Feedback on the Interview -{" "} 32 | {interview.role} Interview 33 |

34 |
35 | 36 |
37 |
38 | {/* Overall Impression */} 39 |
40 | star 41 |

42 | Overall Impression:{" "} 43 | 44 | {feedback?.totalScore} 45 | 46 | /100 47 |

48 |
49 | 50 | {/* Date */} 51 |
52 | calendar 53 |

54 | {feedback?.createdAt 55 | ? dayjs(feedback.createdAt).format("MMM D, YYYY h:mm A") 56 | : "N/A"} 57 |

58 |
59 |
60 |
61 | 62 |
63 | 64 |

{feedback?.finalAssessment}

65 | 66 | {/* Interview Breakdown */} 67 |
68 |

Breakdown of the Interview:

69 | {feedback?.categoryScores?.map((category, index) => ( 70 |
71 |

72 | {index + 1}. {category.name} ({category.score}/100) 73 |

74 |

{category.comment}

75 |
76 | ))} 77 |
78 | 79 |
80 |

Strengths

81 |
    82 | {feedback?.strengths?.map((strength, index) => ( 83 |
  • {strength}
  • 84 | ))} 85 |
86 |
87 | 88 |
89 |

Areas for Improvement

90 |
    91 | {feedback?.areasForImprovement?.map((area, index) => ( 92 |
  • {area}
  • 93 | ))} 94 |
95 |
96 | 97 |
98 | 99 | 100 |
101 |
102 | ); 103 | }; 104 | 105 | export default Feedback; -------------------------------------------------------------------------------- /public/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 🤖 IntervueAI - Precision in Recruitment 3 | ![intervueai](https://github.com/user-attachments/assets/d26c8001-18ba-4777-bcd9-f80c479f3557) 4 | 5 | 6 | 7 | 8 | ## 🌟 Overview 9 | The **IntervueAI** is a next-gen AI-powered mock interview platform built to help students and professionals prepare for interviews in a way that actually feels real. It goes beyond generic questions using your resume, job role, and round type to generate personalized, industry-relevant interviews. 10 | 11 | What makes IntervueAI special is its ability to give smart, AI-generated feedback after each session including performance insights, improvement tips, and evaluation reports so you’re not just practicing, you’re leveling up. 12 | 13 | ## ✨ Features 14 | - 🔐 **User Authentication** 15 | - Sign Up and Sign In using password/email authentication handled by Firebase. 16 | 17 | - 🎛️ **Interactive Dashboard** 18 | - Provides an intuitive interface for managing interview preparations. 19 | 20 | - 🧑‍💻 **No Form-Based Flow** 21 | - Say goodbye to static input forms just log in, speak, and start your interview. It's natural, voice-first interaction from the start. 22 | 23 | - 🤖 **AI-Powered Interview Generation** 24 | - Personalized mock interviews based on your resume, job role, and round type using Vapi voice assistants and Google Gemini. 25 | 26 | 27 | - 🧠 **Digital AI Interviewers** 28 | - Interact with intelligent AI personas inspired by iconic digital characters, making practice more immersive and less robotic. 29 | 30 | - 📝 **Real-Time Transcription** 31 | - Get instant AI-generated feedback after each session, including ratings, improvement tips, and evaluation summaries. 32 | 33 | 34 | 35 | ## 🛠️ Tech Stack 36 | - ⚛ **Next.js** 37 | - 🔥 **Firebase** 38 | - 🎨 **Tailwind CSS** 39 | - 🗣️ **Vapi AI** 40 | - 🧩 **shadcn/ui** 41 | - 🧠 **Google Gemini** 42 | 43 | 44 | ## 🤸 Quick Start 45 | 46 | Follow these steps to set up the project locally on your machine. 47 | 48 | 49 | **Installation** 50 | 51 | Install the project dependencies using npm: 52 | 53 | ```bash 54 | npm install 55 | ``` 56 | 57 | **Set Up Environment Variables** 58 | 59 | Create a new file named `.env.local` in the root of your project and add the following content: 60 | 61 | ```env 62 | NEXT_PUBLIC_VAPI_WEB_TOKEN= 63 | NEXT_PUBLIC_VAPI_WORKFLOW_ID= 64 | 65 | GOOGLE_GENERATIVE_AI_API_KEY= 66 | 67 | NEXT_PUBLIC_BASE_URL= 68 | 69 | NEXT_PUBLIC_FIREBASE_API_KEY= 70 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= 71 | NEXT_PUBLIC_FIREBASE_PROJECT_ID= 72 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= 73 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= 74 | NEXT_PUBLIC_FIREBASE_APP_ID= 75 | 76 | FIREBASE_PROJECT_ID= 77 | FIREBASE_CLIENT_EMAIL= 78 | FIREBASE_PRIVATE_KEY= 79 | ``` 80 | 81 | Replace the placeholder values with your actual **[Firebase](https://firebase.google.com/)**, **[Vapi](https://vapi.ai/?utm_source=youtube&utm_medium=video&utm_campaign=jsmastery_recruitingpractice&utm_content=paid_partner&utm_term=recruitingpractice)** credentials. 82 | 83 | **Running the Project** 84 | 85 | ```bash 86 | npm run dev 87 | ``` 88 | 89 | Open [http://localhost:3000](http://localhost:3000) in your browser to view the project. 90 | 91 | ## 🤝 Contributions 92 | This project is **open-source forever!** Contributions are welcome. Feel free to: 93 | - 🎨 Improve UI/UX 94 | - 🧠 Optimize AI algorithms 95 | - 🗄️ Enhance database efficiency 96 | - 🚀 Add new features 97 | 98 | Fork the repository, make changes, and submit a **pull request**! 99 | 100 | ## 📜 License 101 | This project is licensed under the **MIT License**. 102 | 103 | --- 104 | 105 | ### **🎉 Happy Coding & Best of Luck for Your Interviews! 🚀** 106 | -------------------------------------------------------------------------------- /components/Features.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import {features} from '@/constants/feature' 4 | const Features = () => { 5 | return ( 6 | 7 | {features.map((feature, index) => ( 8 |
9 |
10 |
11 |
12 |
{feature.title}
13 |
{feature.description}
14 |
15 |
16 |
17 |
18 |
19 |
20 | ))} 21 | 22 | ); 23 | } 24 | 25 | const StyledWrapper = styled.div` 26 | display: flex; 27 | flex-direction: column; /* Stack items vertically */ 28 | gap: 2rem; /* spacing between cards */ 29 | align-items: center; 30 | @media (min-width: 640px) { 31 | flex-direction: row; /* Row layout on sm and up */ 32 | flex-wrap: wrap; 33 | justify-content: center; 34 | } 35 | .outer { 36 | width: 300px; 37 | height: 250px; 38 | border-radius: 10px; 39 | padding: 1px; 40 | background: radial-gradient(circle 230px at 0% 0%, #ffffff, #9F79C1); 41 | position: relative; 42 | 43 | 44 | } 45 | 46 | .dot { 47 | width: 5px; 48 | aspect-ratio: 1; 49 | position: absolute; 50 | background-color: #fff; 51 | box-shadow: 0 0 10px #ffffff; 52 | border-radius: 100px; 53 | z-index: 2; 54 | right: 10%; 55 | top: 10%; 56 | animation: moveDot 6s linear infinite; 57 | } 58 | 59 | @keyframes moveDot { 60 | 0%, 61 | 100% { 62 | top: 10%; 63 | right: 10%; 64 | } 65 | 25% { 66 | top: 10%; 67 | right: calc(100% - 35px); 68 | } 69 | 50% { 70 | top: calc(100% - 30px); 71 | right: calc(100% - 35px); 72 | } 73 | 75% { 74 | top: calc(100% - 30px); 75 | right: 10%; 76 | } 77 | } 78 | 79 | .card { 80 | z-index: 1; 81 | width: 100%; 82 | height: 100%; 83 | border-radius: 9px; 84 | border: solid 1px #202222; 85 | background-size: 20px 20px; 86 | background: radial-gradient(circle 280px at 0% 0%, #444444, #0c0d0d); 87 | display: flex; 88 | align-items: center; 89 | justify-content: center; 90 | position: relative; 91 | flex-direction: column; 92 | color: #fff; 93 | } 94 | .ray { 95 | width: 220px; 96 | height: 45px; 97 | border-radius: 100px; 98 | position: absolute; 99 | background-color: #c7c7c7; 100 | opacity: 0.4; 101 | box-shadow: 0 0 50px #fff; 102 | filter: blur(10px); 103 | transform-origin: 10%; 104 | top: 0%; 105 | left: 0; 106 | transform: rotate(40deg); 107 | } 108 | 109 | .card .text { 110 | font-weight: bolder; 111 | font-size: 4rem; 112 | background: linear-gradient(45deg, #000000 4%, #fff, #000); 113 | background-clip: text; 114 | color: transparent; 115 | } 116 | 117 | .line { 118 | width: 100%; 119 | height: 1px; 120 | position: absolute; 121 | background-color: #2c2c2c; 122 | } 123 | .topl { 124 | top: 10%; 125 | background: linear-gradient(90deg, #888888 30%, #1d1f1f 70%); 126 | } 127 | .bottoml { 128 | bottom: 10%; 129 | } 130 | .leftl { 131 | left: 10%; 132 | width: 1px; 133 | height: 100%; 134 | background: linear-gradient(180deg, #747474 30%, #222424 70%); 135 | } 136 | .rightl { 137 | right: 10%; 138 | width: 1px; 139 | height: 100%; 140 | }`; 141 | 142 | export default Features -------------------------------------------------------------------------------- /components/Github.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Github = () => { 5 | return ( 6 | 19 | ); 20 | } 21 | 22 | export default Github -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | FormProvider, 9 | useFormContext, 10 | useFormState, 11 | type ControllerProps, 12 | type FieldPath, 13 | type FieldValues, 14 | } from "react-hook-form" 15 | 16 | import { cn } from "@/lib/utils" 17 | import { Label } from "@/components/ui/label" 18 | 19 | const Form = FormProvider 20 | 21 | type FormFieldContextValue< 22 | TFieldValues extends FieldValues = FieldValues, 23 | TName extends FieldPath = FieldPath, 24 | > = { 25 | name: TName 26 | } 27 | 28 | const FormFieldContext = React.createContext( 29 | {} as FormFieldContextValue 30 | ) 31 | 32 | const FormField = < 33 | TFieldValues extends FieldValues = FieldValues, 34 | TName extends FieldPath = FieldPath, 35 | >({ 36 | ...props 37 | }: ControllerProps) => { 38 | return ( 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | const useFormField = () => { 46 | const fieldContext = React.useContext(FormFieldContext) 47 | const itemContext = React.useContext(FormItemContext) 48 | const { getFieldState } = useFormContext() 49 | const formState = useFormState({ name: fieldContext.name }) 50 | const fieldState = getFieldState(fieldContext.name, formState) 51 | 52 | if (!fieldContext) { 53 | throw new Error("useFormField should be used within ") 54 | } 55 | 56 | const { id } = itemContext 57 | 58 | return { 59 | id, 60 | name: fieldContext.name, 61 | formItemId: `${id}-form-item`, 62 | formDescriptionId: `${id}-form-item-description`, 63 | formMessageId: `${id}-form-item-message`, 64 | ...fieldState, 65 | } 66 | } 67 | 68 | type FormItemContextValue = { 69 | id: string 70 | } 71 | 72 | const FormItemContext = React.createContext( 73 | {} as FormItemContextValue 74 | ) 75 | 76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
86 | 87 | ) 88 | } 89 | 90 | function FormLabel({ 91 | className, 92 | ...props 93 | }: React.ComponentProps) { 94 | const { error, formItemId } = useFormField() 95 | 96 | return ( 97 |