Job- Portal : Full Stack NextJS App

Ayush RajputAyush Rajput
5 min read

Installations

// Create next app
npx create-next-app@latest job-portal

//Install shadcn: use skeleton for showing loading
npx shadcn@latest init

//setup clerk
//1. Create your application at clerk you get keys
//2. Install clerk
npm install @clerk/nextjs
//3.  Create .env.local file and pase the keys in it
//4. Wrap whole layout.js main component in ClerkProvider
  return (
    <ClerkProvider>
      <html lang="en">
        <body
          className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        >
          <Suspense fallback={<Loading/>}>
            <CommonLayout children={children}/>
          </Suspense>
        </body>
    </html>
    </ClerkProvider>
  );
//5. Create middleware.js and copy paste the code from its website : PENDING: public routes handling
//6. Create sign-in / sign-up page  like this [[...sign-up]]

// Install this for icons
npm install lucide-react

AUTHENTICATION using CLERK

//app/sign-up/[[..sign-up]]/page.js
import { SignUp } from "@clerk/nextjs";
export default function SignUpPage(){
    return (
        <div className='w-11/12 min-h-[700px] mx-auto flex justify-center items-center'>
            <SignUp/>   
        </div>
    )
}
// Similary for sign-in , now onclick sign-up we redirect on this page after successfull signup-
// we have to check our user in page.js 
  import { currentUser } from '@clerk/nextjs/server'// in server component

  import { UserButton } from "@clerk/nextjs"
  const user = await currentUser() // always in server component we pass it as prop in client component
// now accoridng to this user we show the menu items in the navbar
// for logout clerk provide us following functionaltiy
  <UserButton afterSignOutUrl="/"/>

Handle Role-Based Login - Recruiter or Candidate

//app/component/on-board/index.js -> import this component in page.js(server component)
'use client'
import { useState } from 'react'
import {
    Tabs,
    TabsContent,
    TabsList,
    TabsTrigger,
} from '../ui/tabs'
import { CommonForm } from '../common-form'
import { candidateOnBoardFormControls, recruiterOnBoardFormControls } from '@/utils'

export function OnBoard(){

    const[currentTab , setCurrentTab] = useState('candidate')
    const [recruiterFormData , setRecruiterFormData] = useState({
        name:'',
        companyName:'',
        companyRole:''
    })
    const [ candidateFormData , setCandiDateFormData] = useState({
        resume:'',
        name:'',
        currentCompany:'',
        currentJobLocation:'',
        currentSalary:'',
        noticePeriod:'',
        skills:'',
        totalExperience:'',
        collegeInfo:'',
        linkedin:'',
        isPremiumUser:''
    })

    function handleTabChange(value){
        setCurrentTab(value)
    }


    return(
        <div className="w-11/12 mx-auto">
            <Tabs value={currentTab} onValueChange={handleTabChange}>
                <div className="w-full">
                    <div className="flex items-baseline justify-between border-b pb-6 pt-24">
                        <h1 className="text-4xl font-bold tracking-tight text-gray-900">
                            Welcome to Onboarding
                        </h1>
                        <TabsList>
                            <TabsTrigger value='candidate'>Candidate</TabsTrigger>
                            <TabsTrigger value='recruiter'>Recruiter</TabsTrigger>
                        </TabsList>
                    </div>
                </div>
                <TabsContent value='candidate'>
                    <CommonForm
                     formControls={candidateOnBoardFormControls}
                     buttonText={'On-Board as Candidate'}
                     formData={candidateFormData}
                     setFormData={setCandiDateFormData}
                    />
                </TabsContent>
                <TabsContent value='recruiter'>
                    <CommonForm
                     formControls={recruiterOnBoardFormControls}
                     buttonText={'On-Board as Recruiter'}
                     formData={recruiterFormData}
                     setFormData={setRecruiterFormData}
                    />
                </TabsContent>

            </Tabs>
        </div>

    )
}

Create models and DB Connection to save this details in DB

Create server actions(controllers) to save/fetch data in db

// creatting recruter profile
"use server"
import dbConnect from "@/config/database";
import RecruiterProfile from "@/models/RecruiterProfile";
import User from "@/models/User";
import { revalidatePath } from "next/cache";

// create profile
export async function createProfileAction(formData , pathToRevalidate){
    await dbConnect();
    revalidatePath(pathToRevalidate);
    const {name , companyName , companyRole ,accountType,isPremiumUser, userId, email} = formData
    try{
        //1. Create recruter profile first that gave use _id that should be store in user 
        const newRecruiterProfile = await RecruiterProfile.create({
            name:name,
            companyName:companyName,
            roleInCompany:companyRole
        })
        // 2. Create the user and reference the recruiter profile
        const newUser = await User.create({
            accountType,
            isPremiumUser,
            userId,
            email,
            recruiterProfileInfo:newRecruiterProfile._id    
        })
        return {
            success: true,
            message: "Profile Created Successfully",
            data: JSON.parse(JSON.stringify(newUser)),
        };
    }
    catch(err){
        console.log(err)
        console.log(accountType)
        return {
            success:false,
            message:"Internal Server Error! Please try again later"
        }
    }
}
// sent data like this from frontend
   async function handleCreateProfileAction(){
        const data = {
            userId: user?.id,
            email:user?.primaryEmailAddress?.emailAddress,
            isPremiumUser:false,
            accountType:"recruiter",
            name:recruiterFormData.name,
            companyName:recruiterFormData.companyName,
            companyRole:recruiterFormData.companyRole,
        }
        const result = await createProfileAction(data, "/on-board")
        console.log(result)
    }

// after this fetch this profile Info and show memebrship page or menu items according to the account type

Create Jobs page for recruiter-create jobs , fetch jobs

Authentication for candidate

// first setup storage public bucket in supabase to store resume of candidate
 const supaBaseClient = createClient(
        'https://da *****************pjn.supabase.co',
        'eyJhbGciOiJIUzI1NiIsInR5*****************GpuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE5NTUwNjEsImV4cCI6MjA2NzUzMTA2MX0.PSR2jjzDJAM6VK6YpYj9grTUTx5gXB9_dFYDrlM6ld8'
    )//supabse projecturl and apikey

    function handleFileChange(e){
        e.preventDefault();
        setFile(e.target.files[0]);
    }

    useEffect(()=>{
        if(file) handleUploadPdfToSupabase()
    },[file])

    async function handleUploadPdfToSupabase(e){
        const{data,error}= await supaBaseClient.storage
         .from("job-portal-public")//bucket name
         .upload(`/public/${file.name}` , file , {
            cacheControl:"3600",
            upsert:false,
         })  // on which path file upload in storage
         console.log(data,error)

         if(data){
            setCandiDateFormData({
                ...candidateFormData,
                resume:data.path // this path store in mongoDB database
            })
         }
    }

// now pass this candidate formData to server and createCandidate profile
export async function fetchProfileAction(id){
    await dbConnect();
    try{
        const response = await User.findOne({userId:id}).populate('recruiterProfileInfo').populate('candidateProfileInfo').exec();
        return {
            success:true,
            message:"Data fetched successfully",
            data:JSON.parse(JSON.stringify(response))
        }
    }
    catch(err){
        console.log(err)
        return {
            success:false,
            message:"Internal Server error! Please try again"
        }
    }

}

Make Job Page For candidate Make drawer for job detail and apply funcitonality

// When recruter click to see resume of candidate then we fetch the resume from supabase and show
// its preview on dashboard
// in activity page
                           {
                                uniqueStatusArray.map((status ,i)=>(
                                    <TabsContent className="flex flex-col gap-4" key={i} value={status}>
                                        {
                                            jobList.filter(jobListItem=>
                                                jobApplications.filter(jobApplicationItem=>jobApplicationItem.status.indexOf(status)>-1)
                                                .findIndex(filteredItemByStatus=>jobListItem?._id ===filteredItemByStatus?.jobID)>-1

                                            ).map(finalFilteredItem=><CommonCard icon={<Rocket size={40}/>} title={finalFilteredItem?.jobTitle} description={finalFilteredItem?.jobDescription} />)
                                        }

                                    </TabsContent>
                                ))
                            }

Use filter to search the job

// create filter menus from filtermenuData
   const filterMenus = filterMenuData.map((item)=>({
    id:item?.id,
    name:item?.label,
    options:[
        ...new Set(filterCategories.map((listItem)=>listItem[item.id])) //// contain all ragisters options , ex: onClick companyName filter all company names appaears same for location and all
    ]
   }))

//Now make menuBar from shadcn
                       <Menubar>
                                {
                                    filterMenus.map((filterMenu,i)=>(
                                        <MenubarMenu key={i}>
                        all filter name\\  <MenubarTrigger>{filterMenu?.name}</MenubarTrigger>
                                            <MenubarContent>
                                                {
                                                    filterMenu.options.map((option,optionIdx)=>(
                                                        <MenubarItem onClick={()=>handleFilter(filterMenu.id , option)} key={optionIdx} className='flex items-center'>
                                             check box\\    <div className="h-4 w-4 border rounded border-gray-900 text-indigo-600"/>
                                                            <Label className="ml-3 cursor-pointer text-sm text-gray-600">{option}</Label>

                                                        </MenubarItem>
                                                    ))
                                                }
                                            </MenubarContent>
                                        </MenubarMenu>
                                    ))
                                }
                            </Menubar>

// How to show only filters jobs below
//npm i query-string
  useEffect(()=>{
    setFilterParams(JSON.parse(sessionStorage.getItem("filterParams"))) // on refreshing page our filter data not gone
   },[])

   export function formUrlQuery({params,dataToAdd}){
    let currentUrl = queryString.parse(params)

    if(Object.keys(dataToAdd).length >0){
        Object.keys(dataToAdd).map(key=>{
            if(dataToAdd[key].length===0) delete currentUrl[key];
            else currentUrl[key]=dataToAdd[key].join(",");
        });
    }
    return queryString.stringifyUrl(
        {
            url:window.location.pathname,
            query: currentUrl
        },
        {
            skipNull:true
        }
    )
}

   useEffect(()=>{
    if(filterParams && Object.keys(filterParams).length>0){
        let url=""
        url=formUrlQuery({
            params:searchParams.toString(),
            dataToAdd:filterParams
        })
        router.push(url , {scroll:false})
    }

   },[filterParams, searchParams])

MemerShip Page - see stripe payment integration in next article

0
Subscribe to my newsletter

Read articles from Ayush Rajput directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ayush Rajput
Ayush Rajput