Integrate complete Candidat platform with ASP.NET chatbot service

- Added Next.js frontend for candidate interviews
- Added Node.js backend with TypeScript and AI integration
- Added ASP.NET Core chatbot service for specialized AI conversations
- Added MySQL database with complete schema
- Added Nginx reverse proxy configuration
- Complete Docker Compose orchestration for all services
- Environment configuration for production, development, and Cloudflare
- Comprehensive documentation and setup instructions
- Flattened nested folder structures for clean organization
- Integrated chatbot service with fallback to direct AI calls
This commit is contained in:
2025-09-20 10:45:21 +02:00
parent bcd25503c5
commit ec8342b5e2
128 changed files with 27581 additions and 10 deletions
+788
View File
@@ -0,0 +1,788 @@
"use client";
import { useState } from 'react';
import { z } from 'zod';
import axios from 'axios';
// Form validation schema
const createJobSchema = z.object({
title: z.string().min(1, 'Job title is required').max(255, 'Title too long'),
description: z.string().min(1, 'Job description is required'),
requirements: z.string().min(1, 'Job requirements are required'),
skills_required: z.array(z.string()).min(1, 'At least one skill is required'),
location: z.string().optional(),
employment_type: z.enum(['full_time', 'part_time', 'contract', 'internship']).default('full_time'),
experience_level: z.enum(['entry', 'mid', 'senior', 'lead', 'executive']).default('mid'),
salary_min: z.number().min(0).optional(),
salary_max: z.number().min(0).optional(),
currency: z.string().length(3).default('USD'),
application_deadline: z.string().optional(),
icon: z.string().optional(),
});
type CreateJobFormData = z.infer<typeof createJobSchema>;
interface CreateJobModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (jobData: CreateJobFormData) => void;
}
export default function CreateJobModal({ isOpen, onClose, onSubmit }: CreateJobModalProps) {
const [formData, setFormData] = useState<CreateJobFormData>({
title: '',
description: '',
requirements: '',
skills_required: [],
location: '',
employment_type: 'full_time',
experience_level: 'mid',
salary_min: undefined,
salary_max: undefined,
currency: 'USD',
application_deadline: '',
icon: 'briefcase',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [skillInput, setSkillInput] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [currentStep, setCurrentStep] = useState(1);
const [shakeAnimation, setShakeAnimation] = useState(false);
const totalSteps = 4;
// Available job icons
const jobIcons = [
{ id: 'briefcase', name: 'Briefcase', emoji: '💼' },
{ id: 'code', name: 'Code', emoji: '💻' },
{ id: 'chart', name: 'Analytics', emoji: '📊' },
{ id: 'design', name: 'Design', emoji: '🎨' },
{ id: 'marketing', name: 'Marketing', emoji: '📈' },
{ id: 'sales', name: 'Sales', emoji: '💼' },
{ id: 'support', name: 'Support', emoji: '🎧' },
{ id: 'engineering', name: 'Engineering', emoji: '⚙️' },
{ id: 'data', name: 'Data', emoji: '📊' },
{ id: 'security', name: 'Security', emoji: '🔒' },
{ id: 'mobile', name: 'Mobile', emoji: '📱' },
{ id: 'cloud', name: 'Cloud', emoji: '☁️' },
];
const handleInputChange = (field: keyof CreateJobFormData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
// Clear shake animation when user starts typing
if (shakeAnimation) {
setShakeAnimation(false);
}
};
const handleAddSkill = () => {
if (skillInput.trim() && !formData.skills_required.includes(skillInput.trim())) {
setFormData(prev => ({
...prev,
skills_required: [...prev.skills_required, skillInput.trim()]
}));
setSkillInput('');
}
};
const handleRemoveSkill = (skillToRemove: string) => {
setFormData(prev => ({
...prev,
skills_required: prev.skills_required.filter(skill => skill !== skillToRemove)
}));
};
const validateCurrentStep = () => {
const newErrors: Record<string, string> = {};
if (currentStep === 1) {
// Validate Step 1: Basic Information
if (!formData.title.trim()) {
newErrors.title = 'Job title is required';
}
// Other fields in step 1 are optional
} else if (currentStep === 2) {
// Validate Step 2: Job Details
if (formData.skills_required.length === 0) {
newErrors.skills_required = 'At least one skill is required';
}
if (!formData.description.trim()) {
newErrors.description = 'Job description is required';
}
if (!formData.requirements.trim()) {
newErrors.requirements = 'Job requirements are required';
}
} else if (currentStep === 3) {
// Validate Step 3: Icon Selection
if (!formData.icon) {
newErrors.icon = 'Please select an icon for this job';
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const nextStep = () => {
if (validateCurrentStep() && currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
} else {
// Trigger shake animation when validation fails
setShakeAnimation(true);
setTimeout(() => setShakeAnimation(false), 500);
}
};
const prevStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
console.log('Form submitted, current step:', currentStep, 'total steps:', totalSteps);
// Only create job if we're on the final step (review)
if (currentStep === totalSteps) {
console.log('Creating job on final step');
await createJob();
} else {
console.log('Not on final step, just validating');
// Just validate and move to next step
if (validateCurrentStep()) {
nextStep();
}
}
};
const createJob = async () => {
setIsSubmitting(true);
try {
console.log('Creating job with form data:', formData);
const validatedData = createJobSchema.parse(formData);
console.log('Validated data:', validatedData);
// Call the API to create the job
const token = localStorage.getItem("token");
console.log('Sending request to backend...');
const response = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/rest/jobs`, validatedData, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('Job created successfully:', response.data);
// Call the parent onSubmit callback
onSubmit(validatedData);
// Reset form
setFormData({
title: '',
description: '',
requirements: '',
skills_required: [],
location: '',
employment_type: 'full_time',
experience_level: 'mid',
salary_min: undefined,
salary_max: undefined,
currency: 'USD',
application_deadline: '',
});
setErrors({});
setCurrentStep(1);
onClose();
} catch (error) {
if (error instanceof z.ZodError) {
const fieldErrors: Record<string, string> = {};
error.issues.forEach((err) => {
if (err.path[0]) {
fieldErrors[err.path[0] as string] = err.message;
}
});
setErrors(fieldErrors);
} else {
console.error('Error creating job:', error);
// Handle API errors
if ((error as any).response) {
// Server responded with error status
console.error('API Error Response:', (error as any).response.data);
console.error('API Error Status:', (error as any).response.status);
console.error('API Error Headers:', (error as any).response.headers);
// Show user-friendly error message
alert(`Failed to create job: ${(error as any).response.data?.message || (error as any).response.statusText || 'Unknown error'}`);
} else if ((error as any).request) {
// Request was made but no response received
console.error('API Error Request:', (error as any).request);
alert('Failed to create job: No response from server. Please check if the backend is running.');
} else {
// Something else happened
console.error('API Error:', (error as any).message);
alert(`Failed to create job: ${(error as any).message}`);
}
}
} finally {
setIsSubmitting(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[95vh] overflow-hidden">
{/* Header with Progress */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 px-8 py-6 text-white">
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-2xl font-bold">Create New Job</h2>
<p className="text-blue-100 mt-1">Step {currentStep} of {totalSteps}</p>
</div>
<button
onClick={onClose}
className="text-blue-200 hover:text-white transition-colors p-2 rounded-lg hover:bg-blue-700"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Progress Bar */}
<div className="w-full bg-blue-500 bg-opacity-30 rounded-full h-2">
<div
className="bg-white h-2 rounded-full transition-all duration-300 ease-out"
style={{ width: `${(currentStep / totalSteps) * 100}%` }}
></div>
</div>
{/* Step Indicators */}
<div className="flex justify-between mt-4">
{[1, 2, 3, 4].map((step) => {
const hasErrors = (step === 1 && errors.title) ||
(step === 2 && (errors.skills_required || errors.description || errors.requirements)) ||
(step === 3 && errors.icon);
return (
//step 3 shall be called "Icon" and step 4 shall be called "Review"
<div key={step} className="flex items-center">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-all duration-200 ${
step <= currentStep
? hasErrors
? 'bg-red-500 text-white animate-pulse'
: 'bg-white text-blue-600'
: 'bg-blue-500 bg-opacity-30 text-blue-200'
}`}>
{hasErrors ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : (
step
)}
</div>
<span className={`ml-2 text-sm transition-colors ${
hasErrors ? 'text-red-200' : 'text-blue-100'
}`}>
{step === 1 ? 'Basic Info' : step === 2 ? 'Details' : step === 3 ? 'Icon' : 'Review'}
</span>
</div>
);
})}
</div>
</div>
{/* Form Content */}
<div className={`p-8 overflow-y-auto max-h-[calc(95vh-200px)] transition-transform duration-500 ${
shakeAnimation ? 'animate-pulse' : ''
}`}>
{/* Validation Error Summary */}
{shakeAnimation && Object.keys(errors).length > 0 && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-start space-x-3">
<svg className="w-5 h-5 text-red-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="text-sm font-medium text-red-800 dark:text-red-200">Please complete the required fields:</h4>
<ul className="mt-2 text-sm text-red-700 dark:text-red-300 space-y-1">
{errors.title && <li> Job title is required</li>}
{errors.skills_required && <li> At least one skill is required</li>}
{errors.description && <li> Job description is required</li>}
{errors.requirements && <li> Job requirements are required</li>}
</ul>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit} id="job-form">
{/* Step 1: Basic Information */}
{currentStep === 1 && (
<div className="space-y-6">
<div className="text-center mb-8">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">Basic Information</h3>
<p className="text-gray-600 dark:text-gray-400">Let's start with the essential details about this position</p>
</div>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Job Title *
</label>
<input
type="text"
value={formData.title}
onChange={(e) => handleInputChange('title', e.target.value)}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white transition-colors ${
errors.title ? 'border-red-500 bg-red-50 dark:bg-red-900/20 ring-2 ring-red-200 dark:ring-red-800' : 'border-gray-300'
}`}
placeholder="e.g. Senior Frontend Developer"
/>
{errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Employment Type
</label>
<select
value={formData.employment_type}
onChange={(e) => handleInputChange('employment_type', e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="full_time">Full Time</option>
<option value="part_time">Part Time</option>
<option value="contract">Contract</option>
<option value="internship">Internship</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Experience Level
</label>
<select
value={formData.experience_level}
onChange={(e) => handleInputChange('experience_level', e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
>
<option value="entry">Entry Level</option>
<option value="mid">Mid Level</option>
<option value="senior">Senior Level</option>
<option value="lead">Lead</option>
<option value="executive">Executive</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Location
</label>
<input
type="text"
value={formData.location}
onChange={(e) => handleInputChange('location', e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="e.g. New York, NY or Remote"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Application Deadline
</label>
<input
type="date"
value={formData.application_deadline}
onChange={(e) => handleInputChange('application_deadline', e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
</div>
</div>
)}
{/* Step 2: Job Details */}
{currentStep === 2 && (
<div className="space-y-6">
<div className="text-center mb-8">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">Job Details</h3>
<p className="text-gray-600 dark:text-gray-400">Add comprehensive details about the role and compensation</p>
</div>
<div className="space-y-6">
{/* Salary Information */}
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg">
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Compensation</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Min Salary
</label>
<input
type="number"
value={formData.salary_min || ''}
onChange={(e) => handleInputChange('salary_min', e.target.value ? Number(e.target.value) : undefined)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
placeholder="50000"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Max Salary
</label>
<input
type="number"
value={formData.salary_max || ''}
onChange={(e) => handleInputChange('salary_max', e.target.value ? Number(e.target.value) : undefined)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
placeholder="80000"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Currency
</label>
<select
value={formData.currency}
onChange={(e) => handleInputChange('currency', e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
<option value="CAD">CAD</option>
</select>
</div>
</div>
</div>
{/* Skills Required */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Required Skills *
</label>
<div className={`flex gap-2 mb-3 p-3 rounded-lg transition-colors ${
errors.skills_required ? 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800' : ''
}`}>
<input
type="text"
value={skillInput}
onChange={(e) => setSkillInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddSkill())}
className={`flex-1 px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white transition-colors ${
errors.skills_required ? 'border-red-500 ring-2 ring-red-200 dark:ring-red-800' : 'border-gray-300'
}`}
placeholder="Add a skill and press Enter"
/>
<button
type="button"
onClick={handleAddSkill}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
>
Add
</button>
</div>
{errors.skills_required && (
<div className="flex items-center space-x-2 text-red-500 text-sm mb-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{errors.skills_required}</span>
</div>
)}
<div className="flex flex-wrap gap-2">
{formData.skills_required.map((skill, index) => (
<span
key={index}
className="inline-flex items-center px-3 py-1 rounded-full text-sm bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
>
{skill}
<button
type="button"
onClick={() => handleRemoveSkill(skill)}
className="ml-2 text-blue-600 dark:text-blue-300 hover:text-blue-800 dark:hover:text-blue-100"
>
×
</button>
</span>
))}
</div>
</div>
{/* Job Description */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Job Description *
</label>
<textarea
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={5}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white transition-colors ${
errors.description ? 'border-red-500 bg-red-50 dark:bg-red-900/20 ring-2 ring-red-200 dark:ring-red-800' : 'border-gray-300'
}`}
placeholder="Describe the role, responsibilities, and what makes this opportunity exciting..."
/>
{errors.description && (
<div className="flex items-center space-x-2 text-red-500 text-sm mt-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{errors.description}</span>
</div>
)}
</div>
{/* Job Requirements */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Job Requirements *
</label>
<textarea
value={formData.requirements}
onChange={(e) => handleInputChange('requirements', e.target.value)}
rows={5}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white transition-colors ${
errors.requirements ? 'border-red-500 bg-red-50 dark:bg-red-900/20 ring-2 ring-red-200 dark:ring-red-800' : 'border-gray-300'
}`}
placeholder="List the specific requirements, qualifications, and experience needed..."
/>
{errors.requirements && (
<div className="flex items-center space-x-2 text-red-500 text-sm mt-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{errors.requirements}</span>
</div>
)}
</div>
</div>
</div>
)}
{/* Step 3: Icon Selection */}
{currentStep === 3 && (
<div className="space-y-6">
<div className="text-center mb-8">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">Choose an Icon</h3>
<p className="text-gray-600 dark:text-gray-400">Select an icon that best represents this job position</p>
</div>
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{jobIcons.map((icon) => (
<button
key={icon.id}
type="button"
onClick={() => handleInputChange('icon', icon.id)}
className={`p-4 rounded-lg border-2 transition-all duration-200 hover:scale-105 ${
formData.icon === icon.id
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-200 dark:ring-blue-800'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
} ${errors.icon ? 'border-red-500 bg-red-50 dark:bg-red-900/20 ring-2 ring-red-200 dark:ring-red-800' : ''}`}
>
<div className="text-3xl mb-2">{icon.emoji}</div>
<div className="text-xs text-gray-600 dark:text-gray-400 text-center">{icon.name}</div>
</button>
))}
</div>
{errors.icon && (
<div className="text-red-500 text-sm text-center">{errors.icon}</div>
)}
</div>
)}
{/* Step 4: Review */}
{currentStep === 4 && (
<div className="space-y-6">
<div className="text-center mb-8">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">Review & Create</h3>
<p className="text-gray-600 dark:text-gray-400">Review your job posting before publishing</p>
</div>
<div className="bg-gray-50 dark:bg-gray-700 p-6 rounded-lg space-y-4">
{/* Job Preview Card */}
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-600">
<div className="flex items-start space-x-4">
<div className="text-4xl">
{jobIcons.find(icon => icon.id === formData.icon)?.emoji || '💼'}
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{formData.title}</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm mt-1 line-clamp-2">
{formData.description?.substring(0, 100)}...
</p>
<div className="flex flex-wrap gap-2 mt-2">
<span className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 text-xs rounded">
{formData.employment_type?.replace('_', ' ')}
</span>
<span className="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-xs rounded">
{formData.experience_level?.replace('_', ' ')}
</span>
{formData.location && (
<span className="px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 text-xs rounded">
{formData.location}
</span>
)}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Job Title</h4>
<p className="text-gray-600 dark:text-gray-400">{formData.title || 'Not specified'}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Employment Type</h4>
<p className="text-gray-600 dark:text-gray-400 capitalize">{formData.employment_type?.replace('_', ' ')}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Experience Level</h4>
<p className="text-gray-600 dark:text-gray-400 capitalize">{formData.experience_level?.replace('_', ' ')}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Location</h4>
<p className="text-gray-600 dark:text-gray-400">{formData.location || 'Not specified'}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Salary Range</h4>
<p className="text-gray-600 dark:text-gray-400">
{formData.salary_min && formData.salary_max
? `${formData.currency} ${formData.salary_min.toLocaleString()} - ${formData.salary_max.toLocaleString()}`
: formData.salary_min
? `${formData.currency} ${formData.salary_min.toLocaleString()}+`
: 'Not specified'
}
</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">Application Deadline</h4>
<p className="text-gray-600 dark:text-gray-400">
{formData.application_deadline
? new Date(formData.application_deadline).toLocaleDateString()
: 'No deadline set'
}
</p>
</div>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Required Skills</h4>
<div className="flex flex-wrap gap-2">
{formData.skills_required.length > 0 ? (
formData.skills_required.map((skill, index) => (
<span key={index} className="px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-sm">
{skill}
</span>
))
) : (
<p className="text-gray-500 dark:text-gray-400">No skills specified</p>
)}
</div>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Description</h4>
<p className="text-gray-600 dark:text-gray-400 whitespace-pre-wrap">{formData.description || 'No description provided'}</p>
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Requirements</h4>
<p className="text-gray-600 dark:text-gray-400 whitespace-pre-wrap">{formData.requirements || 'No requirements specified'}</p>
</div>
</div>
</div>
)}
{/* Navigation Buttons */}
<div className="flex justify-between pt-8 border-t border-gray-200 dark:border-gray-700">
<div>
{currentStep > 1 && (
<button
type="button"
onClick={prevStep}
className="px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors"
>
Previous
</button>
)}
</div>
<div className="flex space-x-4">
<button
type="button"
onClick={onClose}
className="px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 transition-colors"
>
Cancel
</button>
{currentStep < totalSteps ? (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (validateCurrentStep()) {
nextStep();
} else {
setShakeAnimation(true);
setTimeout(() => setShakeAnimation(false), 500);
}
}}
className={`px-6 py-3 text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200 ${
shakeAnimation
? 'bg-red-600 hover:bg-red-700 ring-2 ring-red-300 animate-pulse'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{shakeAnimation ? 'Please fill required fields' : 'Next'}
</button>
) : (
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
>
{isSubmitting ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
<span>Creating...</span>
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span>Create Job</span>
</>
)}
</button>
)}
</div>
</div>
</form>
</div>
</div>
</div>
);
}