Job- Portal : Full Stack NextJS App

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
