jobs and resume

This commit is contained in:
hamid zarghami
2025-10-06 10:15:10 +03:30
parent bcf5431f2b
commit 9ffb33f335
6 changed files with 472 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+254
View File
@@ -0,0 +1,254 @@
import DefaulModal from '@/components/DefaulModal'
import { Button } from '@/components/ui/button'
import { useState, type FC, useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { ResumeType } from '../types/Types'
import { useGetJobs } from '../hooks/useJobsData'
import { useMutation } from '@tanstack/react-query'
import * as api from '../service/JobsService'
import { toast } from '@/components/Toast'
import Input from '@/components/Input'
import DatePickerComponent from '@/components/DatePicker'
import Select from '@/components/Select'
import { DocumentUpload, CloseCircle } from 'iconsax-react'
import { extractErrorMessage } from '@/helpers/errorUtils'
interface ResumeProps {
jobId?: string
}
const Resume: FC<ResumeProps> = ({ jobId }) => {
const [open, setOpen] = useState<boolean>(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [filePreview, setFilePreview] = useState<string>('')
const { data: jobsData } = useGetJobs()
const { register, handleSubmit, formState: { errors }, setValue, watch, reset } = useForm<ResumeType>()
const jobs = jobsData?.results?.jobs || []
const jobOptions = jobs.map(job => ({
label: job.title,
value: job._id
}))
const watchedJob = watch('job')
useEffect(() => {
if (jobId) {
setValue('job', jobId)
}
}, [jobId, setValue])
const uploadFileMutation = useMutation({
mutationFn: api.uploadSingleFile,
onSuccess: (data) => {
setValue('resumeUrl', data?.results?.url?.url)
toast('فایل با موفقیت آپلود شد', 'success')
},
onError: () => {
toast('خطا در آپلود فایل', 'error')
setSelectedFile(null)
setFilePreview('')
}
})
const sendResumeMutation = useMutation({
mutationFn: api.sendResume,
onSuccess: () => {
toast('رزومه شما با موفقیت ارسال شد', 'success')
setOpen(false)
reset()
setSelectedFile(null)
setFilePreview('')
},
onError: (error) => {
toast(extractErrorMessage(error), 'error')
}
})
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
// بررسی نوع فایل
if (!file.type.includes('pdf') && !file.type.includes('word') && !file.type.includes('document')) {
toast('لطفا فایل PDF یا Word انتخاب کنید', 'error')
return
}
// بررسی اندازه فایل (حداکثر 5MB)
if (file.size > 5 * 1024 * 1024) {
toast('حجم فایل نباید بیشتر از ۵ مگابایت باشد', 'error')
return
}
setSelectedFile(file)
setFilePreview(file.name)
// آپلود فایل
uploadFileMutation.mutate(file)
}
}
const removeFile = () => {
setSelectedFile(null)
setFilePreview('')
setValue('resumeUrl', '')
}
const onSubmit = (data: ResumeType) => {
if (!selectedFile) {
toast('لطفا فایل رزومه را انتخاب کنید', 'error')
return
}
if (!data.resumeUrl) {
toast('لطفا منتظر تکمیل آپلود فایل باشید', 'error')
return
}
sendResumeMutation.mutate(data)
}
return (
<div>
<Button onClick={() => setOpen(!open)} size={'sm'}>
ارسال رزومه
</Button>
<DefaulModal
open={open}
close={() => setOpen(false)}
title_header='ارسال رزومه'
isHeader
>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* انتخاب شغل */}
<Select
placeholder="انتخاب شغل"
items={jobOptions}
value={watchedJob}
name="job"
onChange={(e) => setValue('job', e.target.value)}
error_text={errors.job?.message}
disabled={!!jobId}
/>
{/* نام کامل */}
<Input
label="نام کامل"
placeholder="نام کامل خود را وارد کنید"
{...register('fullName', { required: 'نام کامل الزامی است' })}
error_text={errors.fullName?.message}
/>
{/* نام پدر */}
<Input
label="نام پدر"
placeholder="نام پدر خود را وارد کنید"
{...register('fatherName', { required: 'نام پدر الزامی است' })}
error_text={errors.fatherName?.message}
/>
{/* محل تولد */}
<Input
label="محل تولد"
placeholder="محل تولد خود را وارد کنید"
{...register('placeOfBirth', { required: 'محل تولد الزامی است' })}
error_text={errors.placeOfBirth?.message}
/>
{/* تاریخ تولد */}
<DatePickerComponent
label="تاریخ تولد"
placeholder="تاریخ تولد را انتخاب کنید"
onChange={(date) => setValue('birthday', date)}
error_text={errors.birthday?.message}
/>
{/* آپلود فایل رزومه */}
<div className="w-full">
<label className="text-sm text-black block mb-2">
فایل رزومه (PDF یا Word)
</label>
{!selectedFile ? (
<div className="relative">
<input
type="file"
accept=".pdf,.doc,.docx"
onChange={handleFileChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
id="resume-file"
disabled={uploadFileMutation.isPending}
/>
<label
htmlFor="resume-file"
className="flex items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 rounded-xl cursor-pointer hover:border-gray-400 transition-colors"
>
<div className="text-center">
<DocumentUpload color='gray' size={32} className="mx-auto text-gray-400 mb-2" />
<p className="text-sm text-gray-600">
فایل رزومه خود را اینجا رها کنید یا کلیک کنید
</p>
<p className="text-xs text-gray-400 mt-1">
PDF یا Word (حداکثر ۵ مگابایت)
</p>
</div>
</label>
</div>
) : (
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-xl border">
<div className="flex items-center gap-2">
<DocumentUpload color='gray' size={20} className="text-gray-600" />
<span className="text-sm text-gray-700 truncate">
{filePreview}
</span>
{uploadFileMutation.isPending && (
<span className="text-xs text-blue-600">در حال آپلود...</span>
)}
</div>
<button
type="button"
onClick={removeFile}
disabled={uploadFileMutation.isPending}
className="text-red-500 hover:text-red-700 disabled:opacity-50"
>
<CloseCircle color='red' size={20} />
</button>
</div>
)}
{errors.resumeUrl && (
<div className="text-xs text-red-500 mt-1">
{errors.resumeUrl.message}
</div>
)}
</div>
{/* دکمه‌های اقدام */}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
className="flex-1"
>
انصراف
</Button>
<Button
type="submit"
disabled={sendResumeMutation.isPending || uploadFileMutation.isPending || !watch('resumeUrl')}
className="flex-1"
>
{sendResumeMutation.isPending ? 'در حال ارسال...' :
uploadFileMutation.isPending ? 'در حال آپلود فایل...' : 'ارسال رزومه'}
</Button>
</div>
</form>
</DefaulModal>
</div>
)
}
export default Resume
+21
View File
@@ -0,0 +1,21 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/JobsService";
export const useGetJobs = () => {
return useQuery({
queryKey: ["jobs"],
queryFn: api.getJobs,
});
};
export const useSendResume = () => {
return useMutation({
mutationFn: api.sendResume,
});
};
export const useUploadSingleFile = () => {
return useMutation({
mutationFn: api.uploadSingleFile,
});
};
+124
View File
@@ -0,0 +1,124 @@
'use client'
import Layout from '@/hoc/Layout'
import { type FC } from 'react'
import { useGetJobs } from './hooks/useJobsData'
import { Job } from './types/Types'
import PageLoading from '@/components/PageLoading'
import { Location, Briefcase, Calendar } from 'iconsax-react'
import Image from 'next/image'
import Resume from './components/Resume'
const Jobs: FC = () => {
const { data, isLoading, error } = useGetJobs()
if (isLoading) {
return (
<Layout>
<PageLoading />
</Layout>
)
}
if (error) {
return (
<Layout>
<div className="min-h-screen bg-white py-6 px-4 sm:py-8 sm:px-6 md:py-12 md:px-8 lg:px-20">
<div className="text-center">
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-gray-800 mb-4">
خطا در بارگذاری اطلاعات
</h1>
<p className="text-gray-600 text-sm sm:text-base">
متاسفانه در بارگذاری مشاغل مشکلی پیش آمده است. لطفا دوباره تلاش کنید.
</p>
</div>
</div>
</Layout>
)
}
const jobsData: Job[] = data?.results?.jobs || []
return (
<Layout>
<div className="py-6 sm:py-8 sm:px-6 md:py-12 ">
<div className="">
{/* Header Section */}
<div className="mb-8 sm:mb-10 md:mb-12 text-center">
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-gray-800 mb-2 sm:mb-3">
فرصتهای شغلی
</h1>
<p className="text-gray-600 text-sm sm:text-base leading-relaxed mx-auto max-w-3xl">
به تیم ما بپیوندید و در ساخت آینده کسبوکارها مشارکت کنید
</p>
</div>
<Image
src='/images/bannerjobs.png'
alt='jobs'
width={1000}
height={1000}
className='w-full h-auto ' />
{/* Jobs Section */}
<div className="space-y-4 px-4 sm:px-6 md:px-8 lg:px-20 mt-5 sm:space-y-6 max-w-4xl mx-auto">
{jobsData.length === 0 ? (
<div className="text-center py-12">
<Briefcase color='gray' size="48" className="mx-auto text-gray-300 mb-4" />
<p className="text-gray-500 text-sm sm:text-base">
در حال حاضر فرصتی شغلی موجود نیست
</p>
</div>
) : (
<div className="grid mt-10 gap-4 sm:gap-6">
{jobsData.map((job) => (
<div
key={job._id}
className="bg-[#F7F7F7] rounded-lg p-4 sm:p-6"
>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div className="flex-1">
<h3 className="text-lg sm:text-xl font-semibold text-gray-800 mb-2">
{job.title}
</h3>
<p className="text-gray-600 text-sm sm:text-base leading-relaxed mb-4">
{job.description}
</p>
<div className="flex flex-wrap gap-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<Location color='gray' size="16" />
<span>{job.location}</span>
</div>
<div className="flex items-center gap-1">
<Briefcase color='gray' size="16" />
<span>
{job.type === 'fullTime' && 'تمام وقت'}
{job.type === 'partTime' && 'پاره وقت'}
{job.type === 'contract' && 'قراردادی'}
{job.type === 'internship' && 'کارآموزی'}
</span>
</div>
<div className="flex items-center gap-1">
<Calendar color='gray' size="16" />
<span>
{new Date(job.createdAt).toLocaleDateString('fa-IR')}
</span>
</div>
</div>
</div>
<Resume jobId={job._id} />
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</Layout>
)
}
export default Jobs
+19
View File
@@ -0,0 +1,19 @@
import axios from "@/config/axios";
import { JobsResponse, ResumeType, UploadResponse } from "../types/Types";
export const getJobs = async (): Promise<JobsResponse> => {
const { data } = await axios.get("/jobs");
return data;
};
export const sendResume = async (resume: ResumeType): Promise<void> => {
const { data } = await axios.post("/jobs/resume", resume);
return data;
};
export const uploadSingleFile = async (file: File): Promise<UploadResponse> => {
const formData = new FormData();
formData.append("resume", file);
const { data } = await axios.post("/jobs/upload/single", formData);
return data;
};
+54
View File
@@ -0,0 +1,54 @@
export interface Job {
_id: string;
title: string;
description: string;
location: string;
type: "fullTime" | "partTime" | "contract" | "internship";
createdAt: string;
updatedAt: string;
}
export interface Pager {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: boolean;
nextPage: boolean;
}
export interface JobsResponse {
status: number;
success: boolean;
results: {
pager: Pager;
jobs: Job[];
};
}
export type ResumeType = {
job: string;
fullName: string;
fatherName: string;
placeOfBirth: string;
birthday: string;
resumeUrl: string;
};
export interface FileUrl {
originalname: string;
size: number;
url: string;
type: string;
}
export interface UploadResults {
message: string;
url: FileUrl;
}
export interface UploadResponse {
status: number;
success: boolean;
results: UploadResults;
}