jobs and resume
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user