Last active
July 22, 2024 06:02
-
-
Save Vetrivel-VP/0199de6318e70f6caf3ed3299ae7b7c1 to your computer and use it in GitHub Desktop.
Job Portal Helpers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
------------------------------------------------------------------------------------------------------------------------ | |
Google Ai Studio Scripts | |
/* | |
* Install the Generative AI SDK | |
* | |
* $ npm install @google/generative-ai | |
* | |
* See the getting started guide for more information | |
* https://ai.google.dev/gemini-api/docs/get-started/node | |
*/ | |
const { | |
GoogleGenerativeAI, | |
HarmCategory, | |
HarmBlockThreshold, | |
} = require("@google/generative-ai"); | |
// const apiKey = process.env.GEMINI_API_KEY; | |
const genAI = new GoogleGenerativeAI("Your_Api_key"); | |
const model = genAI.getGenerativeModel({ | |
model: "gemini-1.5-flash", | |
}); | |
const generationConfig = { | |
temperature: 1, | |
topP: 0.95, | |
topK: 64, | |
maxOutputTokens: 8192, | |
responseMimeType: "text/plain", | |
}; | |
async function getGenerativeAIResponse(prompt: string) { | |
const safetySettings = [ | |
{ | |
category: HarmCategory.HARM_CATEGORY_HARASSMENT, | |
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, | |
}, | |
{ | |
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, | |
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, | |
}, | |
{ | |
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, | |
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, | |
}, | |
{ | |
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, | |
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, | |
}, | |
]; | |
const chatSession = model.startChat({ | |
generationConfig, | |
safetySettings, | |
history: [], | |
}); | |
const result = await chatSession.sendMessage(prompt); | |
console.log(result.response.text()); | |
return result.response.text().trim().replace(/```/g, ""); | |
} | |
export default getGenerativeAIResponse; | |
------------------------------------------------------------------------------------------------------------------------ | |
Custom Prompts | |
const job_description = `Could you please draft a job requirements document for the position of ${rollname}? The job description should include roles & responsibilities, key features, and details about the role. The required skills should include proficiency in ${skills}. Additionally, you can list any optional skill related to job. Thanks!`; | |
const job_short_description = `Could you craft a concise job description for a ${prompt} position in fewer than 400 characters?`; | |
const job_tags = `Generate an array of top 10 keywords related to the job profession "${prompt}". These keywords should encompass various aspects of the profession, including skills, responsibilities, tools, and technologies commonly associated with it. Aim for a diverse set of keywords that accurately represent the breadth of the profession. Your output should be a list/array of keywords. Just return me the array alone.`; | |
const company_why_join_us = `Create a compelling "Why join us" content piece for ${rollname}. Highlight the unique opportunities, benefits, and experiences that ${rollname} offers to its users. Emphasize the platform's value proposition, such as access to a vast music library, personalized recommendations, exclusive content, community features, and career opportunities for musicians and creators. Tailor the content to attract potential users and illustrate why ${rollname} stands out among other music streaming platforms.`; | |
const company_overview = `Generate an overview content about ${rollname}. Include information about its history, purpose, features, user base, and impact on the industry. Focus on providing a comprehensive yet concise summary suitable for readers unfamiliar with the platform.`; | |
------------------------------------------------------------------------------------------------------------------------ | |
Insertin Category Into MongoDB - Script (seed.ts) | |
const { PrismaClient } = require("@prisma/client"); | |
const database = new PrismaClient(); | |
const main = async () => { | |
try { | |
await database.category.createMany({ | |
data: [ | |
{ name: "Software Development" }, | |
{ name: "Web Development" }, | |
{ name: "Mobile App Development" }, | |
{ name: "Data Science" }, | |
{ name: "Machine Learning" }, | |
{ name: "Artificial Intelligence" }, | |
{ name: "UI/UX Design" }, | |
{ name: "Product Management" }, | |
{ name: "Project Management" }, | |
{ name: "Quality Assurance" }, | |
{ name: "DevOps" }, | |
{ name: "Cybersecurity" }, | |
{ name: "Cloud Computing" }, | |
{ name: "Database Administration" }, | |
{ name: "Network Engineering" }, | |
{ name: "Business Analysis" }, | |
{ name: "Sales" }, | |
{ name: "Marketing" }, | |
{ name: "Customer Support" }, | |
{ name: "Human Resources" }, | |
{ name: "Finance" }, | |
{ name: "Accounting" }, | |
{ name: "Legal" }, | |
], | |
}); | |
console.log("Success"); | |
} catch (error) { | |
console.log(`Error on seeding the database categories : ${error}`); | |
} | |
}; | |
main(); | |
------------------------------------------------------------------------------------------------------------------------ | |
Attachements Form - attachments-form.tsx | |
"use client"; | |
import { AttachmentsUploads } from "@/components/attachments-uploads"; | |
import { ImageUpload } from "@/components/image-upload"; | |
import { Button } from "@/components/ui/button"; | |
import { | |
Form, | |
FormControl, | |
FormField, | |
FormItem, | |
FormMessage, | |
} from "@/components/ui/form"; | |
import { Input } from "@/components/ui/input"; | |
import { zodResolver } from "@hookform/resolvers/zod"; | |
import { Job, Attachment } from "@prisma/client"; | |
import axios from "axios"; | |
import { File, ImageIcon, Pencil, X } from "lucide-react"; | |
import Image from "next/image"; | |
import { useRouter } from "next/navigation"; | |
import { useState } from "react"; | |
import { useForm } from "react-hook-form"; | |
import toast from "react-hot-toast"; | |
import { z } from "zod"; | |
interface AttachmentsFormProps { | |
initialData: Job & { attachments: Attachment[] }; | |
jobId: string; | |
} | |
const formSchema = z.object({ | |
attachments: z.object({ url: z.string(), name: z.string() }).array(), | |
}); | |
export const AttachmentsForm = ({ | |
initialData, | |
jobId, | |
}: AttachmentsFormProps) => { | |
const [isEditing, setIsEditing] = useState(false); | |
const router = useRouter(); | |
// Assuming initialData is available and has type of any | |
const initialAttachments = Array.isArray(initialData?.attachments) | |
? initialData.attachments.map((attachment: any) => { | |
if ( | |
typeof attachment === "object" && | |
attachment !== null && | |
"url" in attachment && | |
"name" in attachment | |
) { | |
return { url: attachment.url, name: attachment.name }; | |
} | |
return { url: "", name: "" }; // Provide default values if the shape is incorrect | |
}) | |
: []; | |
const form = useForm<z.infer<typeof formSchema>>({ | |
resolver: zodResolver(formSchema), | |
defaultValues: { | |
attachments: initialAttachments, | |
}, | |
}); | |
const { isSubmitting, isValid } = form.formState; | |
const onSubmit = async (values: z.infer<typeof formSchema>) => { | |
console.log(values); | |
try { | |
const response = await axios.post( | |
`/api/jobs/${jobId}/attachments`, | |
values | |
); | |
toast.success("Job Attachment updated"); | |
toggleEditing(); | |
router.refresh(); | |
} catch (error) { | |
console.log((error as Error)?.message); | |
toast.error("Something went wrong"); | |
} | |
}; | |
const toggleEditing = () => setIsEditing((current) => !current); | |
return ( | |
<div className="mt-6 border bg-neutral-100 rounded-md p-4"> | |
<div className="font-medium flex items-center justify-between"> | |
Job Attachments | |
<Button onClick={toggleEditing} variant={"ghost"}> | |
{isEditing ? ( | |
<>Cancel</> | |
) : ( | |
<> | |
<Pencil className="w-4 h-4 mr-2" /> | |
Edit | |
</> | |
)} | |
</Button> | |
</div> | |
{/* display the attachments if not editing */} | |
{!isEditing && ( | |
<> | |
{initialData.attachments.map((item) => ( | |
<div | |
key={item.url} | |
className="p-3 w-full bg-purple-100 border-purple-200 border text-purple-700 rounded-md flex items-center" | |
> | |
<File className="w-4 h-4 mr-2 " /> | |
<p className="text-xs w-full truncate">{item.name}</p> | |
<Button | |
variant={"ghost"} | |
size={"icon"} | |
className="p-1" | |
onClick={() => {}} | |
type="button" | |
> | |
<X className="w-4 h4" /> | |
</Button> | |
</div> | |
))} | |
</> | |
)} | |
{/* on editing mode display the input */} | |
{isEditing && ( | |
<Form {...form}> | |
"use client"; | |
import { AttachmentsUploads } from "@/components/attachments-uploads"; | |
import { ImageUpload } from "@/components/image-upload"; | |
import { Button } from "@/components/ui/button"; | |
import { | |
Form, | |
FormControl, | |
FormField, | |
FormItem, | |
FormMessage, | |
} from "@/components/ui/form"; | |
import { Input } from "@/components/ui/input"; | |
import { zodResolver } from "@hookform/resolvers/zod"; | |
import { Job, Attachment } from "@prisma/client"; | |
import axios from "axios"; | |
import { File, ImageIcon, Loader2, Pencil, X } from "lucide-react"; | |
import Image from "next/image"; | |
import { useRouter } from "next/navigation"; | |
import { useState } from "react"; | |
import { useForm } from "react-hook-form"; | |
import toast from "react-hot-toast"; | |
import { z } from "zod"; | |
interface AttachmentsFormProps { | |
initialData: Job & { attachments: Attachment[] }; | |
jobId: string; | |
} | |
const formSchema = z.object({ | |
attachments: z.object({ url: z.string(), name: z.string() }).array(), | |
}); | |
export const AttachmentsForm = ({ | |
initialData, | |
jobId, | |
}: AttachmentsFormProps) => { | |
const [isEditing, setIsEditing] = useState(false); | |
const [deletingId, setDeletingId] = useState<string | null>(null); | |
const router = useRouter(); | |
// Assuming initialData is available and has type of any | |
const initialAttachments = Array.isArray(initialData?.attachments) | |
? initialData.attachments.map((attachment: any) => { | |
if ( | |
typeof attachment === "object" && | |
attachment !== null && | |
"url" in attachment && | |
"name" in attachment | |
) { | |
return { url: attachment.url, name: attachment.name }; | |
} | |
return { url: "", name: "" }; // Provide default values if the shape is incorrect | |
}) | |
: []; | |
const form = useForm<z.infer<typeof formSchema>>({ | |
resolver: zodResolver(formSchema), | |
defaultValues: { | |
attachments: initialAttachments, | |
}, | |
}); | |
const { isSubmitting, isValid } = form.formState; | |
const onSubmit = async (values: z.infer<typeof formSchema>) => { | |
console.log(values); | |
try { | |
const response = await axios.post( | |
`/api/jobs/${jobId}/attachments`, | |
values | |
); | |
toast.success("Job Attachment updated"); | |
toggleEditing(); | |
router.refresh(); | |
} catch (error) { | |
console.log((error as Error)?.message); | |
toast.error("Something went wrong"); | |
} | |
}; | |
const toggleEditing = () => setIsEditing((current) => !current); | |
const onDelete = async (attachment: Attachment) => { | |
try { | |
setDeletingId(attachment.id); | |
await axios.delete(`/api/jobs/${jobId}/attachments/${attachment.id}`); | |
toast.success("Attachment Removed"); | |
router.refresh(); | |
} catch (error) { | |
console.log((error as Error)?.message); | |
toast.error("Something went wrong"); | |
} | |
}; | |
return ( | |
<div className="mt-6 border bg-neutral-100 rounded-md p-4"> | |
<div className="font-medium flex items-center justify-between"> | |
Job Attachments | |
<Button onClick={toggleEditing} variant={"ghost"}> | |
{isEditing ? ( | |
<>Cancel</> | |
) : ( | |
<> | |
<Pencil className="w-4 h-4 mr-2" /> | |
Edit | |
</> | |
)} | |
</Button> | |
</div> | |
{/* display the attachments if not editing */} | |
{!isEditing && ( | |
<div className="space-y-2"> | |
{initialData.attachments.map((item) => ( | |
<div | |
key={item.url} | |
className="p-3 w-full bg-purple-100 border-purple-200 border text-purple-700 rounded-md flex items-center" | |
> | |
<File className="w-4 h-4 mr-2 " /> | |
<p className="text-xs w-full truncate">{item.name}</p> | |
{deletingId === item.id && ( | |
<Button | |
variant={"ghost"} | |
size={"icon"} | |
className="p-1" | |
type="button" | |
> | |
<Loader2 className="h-4 w-4 animate-spin" /> | |
</Button> | |
)} | |
{deletingId !== item.id && ( | |
<Button | |
variant={"ghost"} | |
size={"icon"} | |
className="p-1" | |
onClick={() => { | |
onDelete(item); | |
}} | |
type="button" | |
> | |
<X className="w-4 h4" /> | |
</Button> | |
)} | |
</div> | |
))} | |
</div> | |
)} | |
{/* on editing mode display the input */} | |
{isEditing && ( | |
<Form {...form}> | |
<form | |
onSubmit={form.handleSubmit(onSubmit)} | |
className="space-y-4 mt-4" | |
> | |
<FormField | |
control={form.control} | |
name="attachments" | |
render={({ field }) => ( | |
<FormItem> | |
<FormControl> | |
<AttachmentsUploads | |
value={field.value} | |
disabled={isSubmitting} | |
onChange={(attachments) => { | |
if (attachments) { | |
onSubmit({ attachments }); | |
} | |
}} | |
/> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<div className="flex items-center gap-x-2"> | |
<Button disabled={!isValid || isSubmitting} type="submit"> | |
Save | |
</Button> | |
</div> | |
</form> | |
</Form> | |
)} | |
</div> | |
); | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
Attachements API Route - /api/jobs/[jobId]/attachments/route.ts | |
import { db } from "@/lib/db"; | |
import { auth } from "@clerk/nextjs/server"; | |
import { Attachment } from "@prisma/client"; | |
import { NextResponse } from "next/server"; | |
export const POST = async ( | |
req: Request, | |
{ params }: { params: { jobId: string } } | |
) => { | |
try { | |
const { userId } = auth(); | |
const { jobId } = params; | |
if (!userId) { | |
return new NextResponse("Un-Authorized", { status: 401 }); | |
} | |
if (!jobId) { | |
return new NextResponse("ID Is missing", { status: 401 }); | |
} | |
const { attachments } = await req.json(); | |
if ( | |
!attachments || | |
!Array.isArray(attachments) || | |
attachments.length === 0 | |
) { | |
return new NextResponse("Invalid Attachment Format", { status: 400 }); | |
} | |
const createdAttachments: Attachment[] = []; | |
for (const attachment of attachments) { | |
const { url, name } = attachment; | |
// check the attachment with the same url is already exists for this jobid | |
const existingAttachment = await db.attachment.findFirst({ | |
where: { | |
jobId, | |
url, | |
}, | |
}); | |
if (existingAttachment) { | |
// skip the insertion | |
console.log( | |
`Attachment with URL ${url} already exists for jobId ${jobId}` | |
); | |
continue; | |
} | |
// create a new attachment | |
const createdAttachment = await db.attachment.create({ | |
data: { | |
url, | |
name, | |
jobId, | |
}, | |
}); | |
createdAttachments.push(createdAttachment); | |
} | |
return NextResponse.json(createdAttachments); | |
} catch (error) { | |
console.log(`[JOB_ATTACHMENT_POST] : ${error}`); | |
return new NextResponse("Internal Server Error", { status: 500 }); | |
} | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
Attachement Delete Route - /api/jobs/[jobId]/attachments/[attachmentId]/route.ts | |
import { storage } from "@/config/firebase.config"; | |
import { db } from "@/lib/db"; | |
import { auth } from "@clerk/nextjs/server"; | |
import { deleteObject, ref } from "firebase/storage"; | |
import { NextResponse } from "next/server"; | |
export const DELETE = async ( | |
req: Request, | |
{ params }: { params: { jobId: string; attachmentId: string } } | |
) => { | |
try { | |
const { userId } = auth(); | |
const { jobId, attachmentId } = params; | |
if (!userId) { | |
return new NextResponse("Un-Authorized", { status: 401 }); | |
} | |
if (!jobId) { | |
return new NextResponse("ID Is missing", { status: 401 }); | |
} | |
const attachment = await db.attachment.findUnique({ | |
where: { | |
id: attachmentId, | |
}, | |
}); | |
if (!attachment || attachment.jobId !== params.jobId) { | |
return new NextResponse("Attachment not found", { status: 404 }); | |
} | |
// delete from the firebase storage | |
const storageRef = ref(storage, attachment.url); | |
await deleteObject(storageRef); | |
// delete from mongodb | |
await db.attachment.delete({ | |
where: { | |
id: attachmentId, | |
}, | |
}); | |
return NextResponse.json({ message: "Attachment deleted successfully" }); | |
} catch (error) { | |
console.log(`[JOB_DELETE] : ${error}`); | |
return new NextResponse("Internal Server Error", { status: 500 }); | |
} | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
Attachement Uploads - attachments-uploads.tsx | |
"use client"; | |
import { storage } from "@/config/firebase.config"; | |
import { | |
deleteObject, | |
getDownloadURL, | |
ref, | |
uploadBytesResumable, | |
} from "firebase/storage"; | |
import { File, FilePlus, ImagePlus, Trash, X } from "lucide-react"; | |
import Image from "next/image"; | |
import { useEffect, useState } from "react"; | |
import toast from "react-hot-toast"; | |
import { Button } from "./ui/button"; | |
import { url } from "inspector"; | |
interface AttachmentsUploadsProps { | |
disabled?: boolean; | |
onChange: (value: { url: string; name: string }[]) => void; | |
value: { url: string; name: string }[]; | |
} | |
export const AttachmentsUploads = ({ | |
disabled, | |
onChange, | |
value, | |
}: AttachmentsUploadsProps) => { | |
const [isMounted, setIsMounted] = useState(false); | |
const [isLoading, setIsLoading] = useState(false); | |
const [progress, setProgress] = useState<number>(0); | |
useEffect(() => { | |
setIsMounted(true); | |
}, []); | |
if (!isMounted) { | |
return null; | |
} | |
const onUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { | |
const files: File[] = Array.from(e.target.files || []); | |
setIsLoading(true); | |
// array to store newly uploaded urls | |
const newUrls: { url: string; name: string }[] = []; | |
// counter to keep track the uploaded files | |
let completedFiles = 0; | |
files.forEach((file: File) => { | |
const uploadTask = uploadBytesResumable( | |
ref(storage, `Attachments/${Date.now()}-${file.name}`), | |
file, | |
{ contentType: file.type } | |
); | |
uploadTask.on( | |
"state_changed", | |
(snapshot) => { | |
setProgress((snapshot.bytesTransferred / snapshot.totalBytes) * 100); | |
}, | |
(error) => { | |
toast.error(error.message); | |
}, | |
() => { | |
getDownloadURL(uploadTask.snapshot.ref).then((downloadurl) => { | |
// store this url | |
newUrls.push({ url: downloadurl, name: file.name }); | |
// increase the count of the counter | |
completedFiles++; | |
// check the files are uploaded or not | |
if (completedFiles === files.length) { | |
setIsLoading(false); | |
onChange([...value, ...newUrls]); | |
} | |
}); | |
} | |
); | |
}); | |
}; | |
return ( | |
<div> | |
<div className="w-full h-40 bg-purple-100 p-2 flex items-center justify-center"> | |
{isLoading ? ( | |
<> | |
<p>{`${progress.toFixed(2)}%`}</p> | |
</> | |
) : ( | |
<> | |
<label className="w-full h-full flex items-center justify-center"> | |
<div className="flex gap-2 items-center justify-center cursor-pointer "> | |
<FilePlus className="w-3 h-3 mr-2" /> | |
<p>Add a file</p> | |
</div> | |
<input | |
type="file" | |
accept=".jpg,.jpeg,.png,.gif,.bmp,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.rtf,.odt" | |
multiple | |
className="w-0 h-0" | |
onChange={onUpload} | |
/> | |
</label> | |
</> | |
)} | |
</div> | |
</div> | |
); | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
DateFilterData - date-filter.tsx | |
const DateFilter = () => { | |
const data = [ | |
{ value: "today", label: "Today" }, | |
{ value: "yesterday", label: "Yesterday" }, | |
{ value: "thisWeek", label: "This Week" }, | |
{ value: "lastWeek", label: "Last Week" }, | |
{ value: "thisMonth", label: "This Month" }, | |
]; | |
------------------------------------------------------------------------------------------------------------------------ | |
------------------------------------------------------------------------------------------------------------------------ | |
Filters data | |
const shiftTimingsData = [ | |
{ | |
value: "full-time", | |
label: "Full Time", | |
}, | |
{ | |
value: "part-time", | |
label: "Part Time", | |
}, | |
{ | |
value: "contract", | |
label: "Contract", | |
}, | |
]; | |
const workingModesData = [ | |
{ | |
value: "remote", | |
label: "Remote", | |
}, | |
{ | |
value: "hybrid", | |
label: "Hybrid", | |
}, | |
{ | |
value: "office", | |
label: "Office", | |
}, | |
]; | |
const experienceData = [ | |
{ | |
value: "0", | |
label: "Fresher", | |
}, | |
{ | |
value: "2", | |
label: "0-2 years", | |
}, | |
{ | |
value: "3", | |
label: "2-4 years", | |
}, | |
{ | |
value: "5", | |
label: "5+ years", | |
}, | |
]; | |
------------------------------------------------------------------------------------------------------------------------ | |
resume-form.tsx | |
"use client"; | |
import { AttachmentsUploads } from "@/components/attachments-uploads"; | |
import { ImageUpload } from "@/components/image-upload"; | |
import { Button } from "@/components/ui/button"; | |
import { | |
Form, | |
FormControl, | |
FormField, | |
FormItem, | |
FormMessage, | |
} from "@/components/ui/form"; | |
import { Input } from "@/components/ui/input"; | |
import { cn } from "@/lib/utils"; | |
import { zodResolver } from "@hookform/resolvers/zod"; | |
import { Job, Attachment, UserProfile, Resumes } from "@prisma/client"; | |
import axios from "axios"; | |
import { | |
File, | |
ImageIcon, | |
Loader2, | |
Pencil, | |
PlusCircle, | |
ShieldCheck, | |
ShieldX, | |
X, | |
} from "lucide-react"; | |
import Image from "next/image"; | |
import { useRouter } from "next/navigation"; | |
import { useState } from "react"; | |
import { useForm } from "react-hook-form"; | |
import toast from "react-hot-toast"; | |
import { z } from "zod"; | |
interface ResumeFormProps { | |
initialData: (UserProfile & { resumes: Resumes[] }) | null; | |
userId: string; | |
} | |
const formSchema = z.object({ | |
resumes: z.object({ url: z.string(), name: z.string() }).array(), | |
}); | |
export const ResumeForm = ({ initialData, userId }: ResumeFormProps) => { | |
const [isEditing, setIsEditing] = useState(false); | |
const [deletingId, setDeletingId] = useState<string | null>(null); | |
const [isActiveResumeId, setIsActiveResumeId] = useState<string | null>(null); | |
const router = useRouter(); | |
// Assuming initialData is available and has type of any | |
const initialResumes = Array.isArray(initialData?.resumes) | |
? initialData.resumes.map((resume: any) => { | |
if ( | |
typeof resume === "object" && | |
resume !== null && | |
"url" in resume && | |
"name" in resume | |
) { | |
return { url: resume.url, name: resume.name }; | |
} | |
return { url: "", name: "" }; // Provide default values if the shape is incorrect | |
}) | |
: []; | |
const form = useForm<z.infer<typeof formSchema>>({ | |
resolver: zodResolver(formSchema), | |
defaultValues: { | |
resumes: initialResumes, | |
}, | |
}); | |
const { isSubmitting, isValid } = form.formState; | |
const onSubmit = async (values: z.infer<typeof formSchema>) => { | |
console.log(values, userId); | |
try { | |
const response = await axios.post(`/api/users/${userId}/resumes`, values); | |
toast.success("Resume updated"); | |
toggleEditing(); | |
router.refresh(); | |
} catch (error) { | |
console.log((error as Error)?.message); | |
toast.error("Something went wrong"); | |
} | |
}; | |
const toggleEditing = () => setIsEditing((current) => !current); | |
const onDelete = async (resume: Resumes) => { | |
try { | |
setDeletingId(resume.id); | |
if (initialData?.activeResumeId === resume.id) { | |
toast.error("Can't Delete the active resume"); | |
return; | |
} | |
await axios.delete(`/api/users/${userId}/resumes/${resume.id}`); | |
toast.success("Resume Removed"); | |
router.refresh(); | |
} catch (error) { | |
console.log((error as Error)?.message); | |
toast.error("Something went wrong"); | |
} finally { | |
setDeletingId(null); | |
} | |
}; | |
const setActiveResumeId = async (resumeId: string) => { | |
setIsActiveResumeId(resumeId); | |
const response = await axios.patch(`/api/users/${userId}`, { | |
activeResumeId: resumeId, | |
}); | |
toast.success("Resume Activated"); | |
router.refresh(); | |
try { | |
} catch (error) { | |
console.log((error as Error)?.message); | |
toast.error("Something went wrong"); | |
} finally { | |
setIsActiveResumeId(null); | |
} | |
}; | |
return ( | |
<div className="mt-6 border flex-1 w-full rounded-md p-4"> | |
<div className="font-medium flex items-center justify-between"> | |
Your Resumes | |
<Button onClick={toggleEditing} variant={"ghost"}> | |
{isEditing ? ( | |
<>Cancel</> | |
) : ( | |
<> | |
<PlusCircle className="w-4 h-4 mr-2" /> | |
Add a file | |
</> | |
)} | |
</Button> | |
</div> | |
{/* display the attachments if not editing */} | |
{!isEditing && ( | |
<div className="space-y-2"> | |
{initialData?.resumes.map((item) => ( | |
<div className="grid grid-cols-12 gap-2"> | |
<div | |
key={item.url} | |
className="p-3 w-full bg-purple-100 border-purple-200 border text-purple-700 rounded-md flex items-center col-span-10" | |
> | |
<File className="w-4 h-4 mr-2 " /> | |
<p className="text-xs w-full truncate">{item.name}</p> | |
{deletingId === item.id && ( | |
<Button | |
variant={"ghost"} | |
size={"icon"} | |
className="p-1" | |
type="button" | |
> | |
<Loader2 className="h-4 w-4 animate-spin" /> | |
</Button> | |
)} | |
{deletingId !== item.id && ( | |
<Button | |
variant={"ghost"} | |
size={"icon"} | |
className="p-1" | |
onClick={() => { | |
onDelete(item); | |
}} | |
type="button" | |
> | |
<X className="w-4 h4" /> | |
</Button> | |
)} | |
</div> | |
<div className="col-span-2 flex items-center justify-start gap-2"> | |
{isActiveResumeId === item.id ? ( | |
<> | |
<div className="flex items-center justify-center w-full"> | |
<Loader2 className="w-4 h-4 animate-spin" /> | |
</div> | |
</> | |
) : ( | |
<> | |
<Button | |
variant={"ghost"} | |
className={cn( | |
"flex items-center justify-center", | |
initialData.activeResumeId === item.id | |
? "text-emerald-500" | |
: "text-red-500" | |
)} | |
onClick={() => setActiveResumeId(item.id)} | |
> | |
<p> | |
{initialData.activeResumeId === item.id | |
? "Live" | |
: "Activate"} | |
</p> | |
{initialData.activeResumeId === item.id ? ( | |
<ShieldCheck className="w-4 h-4 ml-2" /> | |
) : ( | |
<ShieldX className="w-4 h-4 ml-2" /> | |
)} | |
</Button> | |
</> | |
)} | |
</div> | |
</div> | |
))} | |
</div> | |
)} | |
{/* on editing mode display the input */} | |
{isEditing && ( | |
<Form {...form}> | |
<form | |
onSubmit={form.handleSubmit(onSubmit)} | |
className="space-y-4 mt-4" | |
> | |
<FormField | |
control={form.control} | |
name="resumes" | |
render={({ field }) => ( | |
<FormItem> | |
<FormControl> | |
<AttachmentsUploads | |
value={field.value} | |
disabled={isSubmitting} | |
onChange={(resumes) => { | |
if (resumes) { | |
onSubmit({ resumes }); | |
} | |
}} | |
/> | |
</FormControl> | |
<FormMessage /> | |
</FormItem> | |
)} | |
/> | |
<div className="flex items-center gap-x-2"> | |
<Button disabled={!isValid || isSubmitting} type="submit"> | |
Save | |
</Button> | |
</div> | |
</form> | |
</Form> | |
)} | |
</div> | |
); | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
/api/users/[userId]/route.ts | |
import { db } from "@/lib/db"; | |
import { auth } from "@clerk/nextjs/server"; | |
import { NextResponse } from "next/server"; | |
export const PATCH = async (req: Request) => { | |
try { | |
const { userId } = auth(); | |
const values = await req.json(); | |
if (!userId) { | |
return new NextResponse("Un-Authorized", { status: 401 }); | |
} | |
let profile = await db.userProfile.findUnique({ | |
where: { | |
userId, | |
}, | |
}); | |
let userProfile; | |
if (profile) { | |
userProfile = await db.userProfile.update({ | |
where: { | |
userId, | |
}, | |
data: { | |
...values, | |
}, | |
}); | |
} else { | |
userProfile = await db.userProfile.create({ | |
data: { | |
userId, | |
...values, | |
}, | |
}); | |
} | |
return NextResponse.json(userProfile); | |
} catch (error) { | |
console.log(`[JOB_PATCH] : ${error}`); | |
return new NextResponse("Internal Server Error", { status: 500 }); | |
} | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
/api/users/[userId]/resumes/route.ts | |
import { db } from "@/lib/db"; | |
import { auth } from "@clerk/nextjs/server"; | |
import { Attachment, Resumes } from "@prisma/client"; | |
import { NextResponse } from "next/server"; | |
export const POST = async (req: Request) => { | |
try { | |
const { userId } = auth(); | |
if (!userId) { | |
return new NextResponse("Un-Authorized", { status: 401 }); | |
} | |
const { resumes } = await req.json(); | |
if (!resumes || !Array.isArray(resumes) || resumes.length === 0) { | |
return new NextResponse("Invalid Resume Format", { status: 400 }); | |
} | |
const createdResumes: Resumes[] = []; | |
for (const resume of resumes) { | |
const { url, name } = resume; | |
// check the resume with the same url is already exists for this resumeId | |
const existingresume = await db.resumes.findFirst({ | |
where: { | |
userProfileId: userId, | |
url, | |
}, | |
}); | |
if (existingresume) { | |
// skip the insertion | |
console.log( | |
`Resume with URL ${url} already exists for resumeId ${userId}` | |
); | |
continue; | |
} | |
// create a new resume | |
const createdResume = await db.resumes.create({ | |
data: { | |
url, | |
name, | |
userProfileId: userId, | |
}, | |
}); | |
createdResumes.push(createdResume); | |
} | |
return NextResponse.json(createdResumes); | |
} catch (error) { | |
console.log(`[USER_RESUME_POST] : ${error}`); | |
return new NextResponse("Internal Server Error", { status: 500 }); | |
} | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
/api/users/[userId]/resumes/[resumeId]route.ts | |
import { storage } from "@/config/firebase.config"; | |
import { db } from "@/lib/db"; | |
import { auth } from "@clerk/nextjs/server"; | |
import { deleteObject, ref } from "firebase/storage"; | |
import { NextResponse } from "next/server"; | |
export const DELETE = async ( | |
req: Request, | |
{ params }: { params: { resumeId: string } } | |
) => { | |
try { | |
const { userId } = auth(); | |
const { resumeId } = params; | |
if (!userId) { | |
return new NextResponse("Un-Authorized", { status: 401 }); | |
} | |
const resume = await db.resumes.findUnique({ | |
where: { | |
id: resumeId, | |
}, | |
}); | |
if (!resume || resume.id !== resumeId) { | |
return new NextResponse("resume not found", { status: 404 }); | |
} | |
// delete from the firebase storage | |
const storageRef = ref(storage, resume.url); | |
await deleteObject(storageRef); | |
// delete from mongodb | |
await db.resumes.delete({ | |
where: { | |
id: resumeId, | |
}, | |
}); | |
return NextResponse.json({ message: "Resume deleted successfully" }); | |
} catch (error) { | |
console.log(`[RESUME_DELETE] : ${error}`); | |
return new NextResponse("Internal Server Error", { status: 500 }); | |
} | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
import { | |
Code, | |
Monitor, | |
Smartphone, | |
BarChart, | |
Cpu, | |
Brain, | |
Palette, | |
Box, | |
Clipboard, | |
Shield, | |
Terminal, | |
Lock, | |
Cloud, | |
Database, | |
Globe, | |
FileText, | |
DollarSign, | |
CreditCard, | |
Headphones, | |
Users, | |
Currency, | |
Scale, | |
LucideIcon, | |
} from "lucide-react"; | |
export type IconName = | |
| "Software Development" | |
| "Web Development" | |
| "Mobile App Development" | |
| "Data Science" | |
| "Machine Learning" | |
| "Artificial Intelligence" | |
| "UI/UX Design" | |
| "Product Management" | |
| "Project Management" | |
| "Quality Assurance" | |
| "DevOps" | |
| "Cybersecurity" | |
| "Cloud Computing" | |
| "Database Administration" | |
| "Network Engineering" | |
| "Business Analysis" | |
| "Sales" | |
| "Marketing" | |
| "Customer Support" | |
| "Human Resources" | |
| "Finance" | |
| "Accounting" | |
| "Legal"; | |
export const iconMapping: Record<IconName, LucideIcon> = { | |
"Software Development": Code, | |
"Web Development": Monitor, | |
"Mobile App Development": Smartphone, | |
"Data Science": BarChart, | |
"Machine Learning": Cpu, | |
"Artificial Intelligence": Brain, | |
"UI/UX Design": Palette, | |
"Product Management": Box, | |
"Project Management": Clipboard, | |
"Quality Assurance": Shield, | |
DevOps: Terminal, | |
Cybersecurity: Lock, | |
"Cloud Computing": Cloud, | |
"Database Administration": Database, | |
"Network Engineering": Globe, | |
"Business Analysis": FileText, | |
Sales: DollarSign, | |
Marketing: CreditCard, | |
"Customer Support": Headphones, | |
"Human Resources": Users, | |
Finance: Currency, | |
Accounting: CreditCard, | |
Legal: Scale, | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
footer.tsx | |
"use client"; | |
import { Logo } from "@/app/(dashboard)/_components/logo"; | |
import Box from "./box"; | |
import { Facebook, Linkedin, Twitter, Youtube } from "lucide-react"; | |
import Link from "next/link"; | |
import { Card, CardDescription, CardTitle } from "./ui/card"; | |
import Image from "next/image"; | |
import { Separator } from "./ui/separator"; | |
const menuOne = [ | |
{ href: "#", label: "About Us" }, | |
{ href: "#", label: "Careers" }, | |
{ href: "#", label: "Employer home" }, | |
{ href: "#", label: "Sitemap" }, | |
{ href: "#", label: "Credits" }, | |
]; | |
export const Footer = () => { | |
return ( | |
<Box className="h-72 p-4 items-start flex-col"> | |
<div className="w-full grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-6"> | |
{/* first section */} | |
<Box className="flex-col items-start gap-6"> | |
<div className="flex items-center gap-3"> | |
<Logo /> | |
<h2 className="text-xl font-semibold text-muted-foreground"> | |
WorkNow | |
</h2> | |
</div> | |
<p className="font-semibold text-base">Connect with us</p> | |
<div className="flex items-center gap-6 w-full"> | |
<Link href={"www.facebook.com"}> | |
<Facebook className="w-5 h-5 text-muted-foreground hover:text-purple-500 hover:scale-125 transition-all" /> | |
</Link> | |
<Link href={"www.facebook.com"}> | |
<Twitter className="w-5 h-5 text-muted-foreground hover:text-purple-500 hover:scale-125 transition-all" /> | |
</Link> | |
<Link href={"www.facebook.com"}> | |
<Linkedin className="w-5 h-5 text-muted-foreground hover:text-purple-500 hover:scale-125 transition-all" /> | |
</Link> | |
<Link href={"www.facebook.com"}> | |
<Youtube className="w-5 h-5 text-muted-foreground hover:text-purple-500 hover:scale-125 transition-all" /> | |
</Link> | |
</div> | |
</Box> | |
{/* second */} | |
<Box className="flex-col items-start justify-between gap-y-4 ml-4"> | |
{menuOne.map((item) => ( | |
<Link key={item.label} href={item.href}> | |
<p className="text-sm font-sans text-neutral-500 hover:text-purple-500"> | |
{item.label} | |
</p> | |
</Link> | |
))} | |
</Box> | |
<Box className="flex-col items-start justify-between gap-y-4 ml-4"> | |
{menuOne.map((item) => ( | |
<Link key={item.label} href={item.href}> | |
<p className="text-sm font-sans text-neutral-500 hover:text-purple-500"> | |
{item.label} | |
</p> | |
</Link> | |
))} | |
</Box> | |
<Card className="p-6 col-span-2"> | |
<CardTitle className="text-base">Apply on the go</CardTitle> | |
<CardDescription> | |
Get real-time job updates on our App | |
</CardDescription> | |
<Link href={"#"}> | |
<div className="w-full relative overflow-hidden h-16"> | |
<Image | |
src={"/img/play-apple-store.png"} | |
fill | |
className="w-full h-full object-contain" | |
alt="Play Store & Apple Store" | |
/> | |
</div> | |
</Link> | |
</Card> | |
</div> | |
<Separator /> | |
<Box className="justify-center p-4 text-sm text-muted-foreground"> | |
All rights reserved © 2024 | |
</Box> | |
</Box> | |
); | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
const PURPLE_COLORS = [ | |
"#8a2be2", // Blue Violet | |
"#9932cc", // Dark Orchid | |
"#a020f0", // Purple | |
"#9370db", // Medium Purple | |
"#ba55d3", // Medium Orchid | |
"#8b008b", // Dark Magenta | |
"#800080", // Purple | |
"#9400d3", // Dark Violet | |
"#9932cc", // Dark Orchid | |
"#800080", // Purple | |
]; | |
------------------------------------------------------------------------------------------------------------------------ | |
list-item.tsx | |
"use client"; | |
import { cn } from "@/lib/utils"; | |
import { Check } from "lucide-react"; | |
interface ListItemProps { | |
category: any; | |
onSelect: (category: any) => void; | |
isChecked: boolean; | |
} | |
export const ListItem = ({ category, onSelect, isChecked }: ListItemProps) => { | |
return ( | |
<div | |
className="flex items-center px-2 py-1 cursor-pointer hover:bg-gray-50 text-muted-foreground hover:text-primary" | |
onClick={() => onSelect(category)} | |
> | |
<Check | |
className={cn( | |
"ml-auto h-4 w-4", | |
isChecked ? "opacity-100" : "opacity-0" | |
)} | |
/> | |
<p className="w-full truncate text-sm whitespace-nowrap"> | |
{category.label} | |
</p> | |
</div> | |
); | |
}; | |
------------------------------------------------------------------------------------------------------------------------ | |
combo-box.tsx | |
"use client"; | |
import * as React from "react"; | |
import { Check, ChevronsUpDown, Search } from "lucide-react"; | |
import { cn } from "@/lib/utils"; | |
import { Button } from "@/components/ui/button"; | |
import { | |
Command, | |
CommandEmpty, | |
CommandGroup, | |
CommandInput, | |
CommandItem, | |
CommandList, | |
} from "@/components/ui/command"; | |
import { | |
Popover, | |
PopoverContent, | |
PopoverTrigger, | |
} from "@/components/ui/popover"; | |
import { ListItem } from "./list-item"; | |
interface ComboboxProps { | |
options: { label: string; value: string }[]; | |
value?: string; | |
onChange: (value: string) => void; | |
heading: string; | |
} | |
export const Combobox = ({ | |
options, | |
value, | |
onChange, | |
heading, | |
}: ComboboxProps) => { | |
const [open, setOpen] = React.useState(false); | |
const [searchTerm, setSearchTerm] = React.useState(""); | |
const [filtered, setFiltered] = React.useState< | |
{ label: string; value: string }[] | |
>([]); | |
const handleSearchTerm = (e: any) => { | |
setSearchTerm(e.target.value); | |
setFiltered( | |
options.filter((item) => | |
item.label.toLowerCase().includes(searchTerm.toLowerCase()) | |
) | |
); | |
}; | |
return ( | |
<Popover open={open} onOpenChange={setOpen}> | |
<PopoverTrigger asChild> | |
<Button | |
variant="outline" | |
role="combobox" | |
aria-expanded={open} | |
className="w-full justify-between" | |
> | |
{value | |
? options.find((option) => option.value === value)?.label | |
: "Select option..."} | |
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> | |
</Button> | |
</PopoverTrigger> | |
<PopoverContent className="w-full p-0 md:min-w-96"> | |
<Command> | |
<div className="w-full px-2 py-1 flex items-center border rounded-md border-gray-100"> | |
<Search className="mr-2 h-4 w-4 min-w-4" /> | |
<input | |
type="text" | |
placeholder="Search category" | |
onChange={handleSearchTerm} | |
className="flex-1 w-full outline-none text-sm py-1" | |
/> | |
</div> | |
<CommandList> | |
<CommandGroup heading={heading}> | |
{searchTerm === "" ? ( | |
options.map((option) => ( | |
<ListItem | |
key={option.value} | |
category={option} | |
onSelect={() => { | |
onChange(option.value === value ? "" : option.value); | |
setOpen(false); | |
}} | |
isChecked={option?.value === value} | |
/> | |
)) | |
) : filtered.length > 0 ? ( | |
filtered.map((option) => ( | |
<ListItem | |
key={option.value} | |
category={option} | |
onSelect={() => { | |
onChange(option.value === value ? "" : option.value); | |
setOpen(false); | |
}} | |
isChecked={option?.value === value} | |
/> | |
)) | |
) : ( | |
<CommandEmpty>No Category Found</CommandEmpty> | |
)} | |
</CommandGroup> | |
</CommandList> | |
</Command> | |
</PopoverContent> | |
</Popover> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment