mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 17:18:09 +00:00
Vacancy added
This commit is contained in:
302
components/VacancyApplicationForm.tsx
Normal file
302
components/VacancyApplicationForm.tsx
Normal file
@ -0,0 +1,302 @@
|
||||
// src/components/VacancyApplicationForm.tsx (Example structure and styling)
|
||||
"use client"; // This likely needs to be a client component for form handling
|
||||
|
||||
import React, { useState, useCallback, ChangeEvent, DragEvent } from "react";
|
||||
import { FaUpload, FaFileAlt, FaTimes } from "react-icons/fa"; // Import icons
|
||||
|
||||
interface VacancyApplicationFormProps {
|
||||
vacancyId: string;
|
||||
vacancyTitle: string;
|
||||
// onFormClose?: () => void; // Optional: If you add a close button
|
||||
}
|
||||
|
||||
export default function VacancyApplicationForm({
|
||||
vacancyId,
|
||||
vacancyTitle,
|
||||
}: VacancyApplicationFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
linkedin: "",
|
||||
coverLetter: "",
|
||||
// Add other fields as needed
|
||||
});
|
||||
const [resumeFile, setResumeFile] = useState<File | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<
|
||||
"idle" | "success" | "error"
|
||||
>("idle");
|
||||
|
||||
const handleInputChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
// --- File Input Handling ---
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setResumeFile(e.target.files[0]);
|
||||
e.target.value = ""; // Reset input value for potential re-upload
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnter = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault(); // Necessary to allow drop
|
||||
e.stopPropagation();
|
||||
if (!isDragging) setIsDragging(true); // Ensure state is true while over
|
||||
},
|
||||
[isDragging]
|
||||
);
|
||||
|
||||
const handleDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
setResumeFile(e.dataTransfer.files[0]);
|
||||
e.dataTransfer.clearData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const removeFile = () => {
|
||||
setResumeFile(null);
|
||||
};
|
||||
// --- End File Input Handling ---
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setSubmitStatus("idle");
|
||||
|
||||
const data = new FormData();
|
||||
data.append("vacancyId", vacancyId);
|
||||
data.append("vacancyTitle", vacancyTitle);
|
||||
data.append("name", formData.name);
|
||||
data.append("email", formData.email);
|
||||
data.append("phone", formData.phone);
|
||||
data.append("linkedin", formData.linkedin);
|
||||
data.append("coverLetter", formData.coverLetter);
|
||||
if (resumeFile) {
|
||||
data.append("resume", resumeFile, resumeFile.name);
|
||||
}
|
||||
|
||||
try {
|
||||
// Replace with your actual API endpoint for form submission
|
||||
const response = await fetch("/api/apply", {
|
||||
method: "POST",
|
||||
body: data, // FormData handles multipart/form-data automatically
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Submission failed");
|
||||
}
|
||||
|
||||
setSubmitStatus("success");
|
||||
// Optionally reset form:
|
||||
// setFormData({ name: '', email: '', ... });
|
||||
// setResumeFile(null);
|
||||
// Optionally close form: onFormClose?.();
|
||||
} catch (error) {
|
||||
console.error("Application submission error:", error);
|
||||
setSubmitStatus("error");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Base Input Styling ---
|
||||
const inputBaseStyle =
|
||||
"block w-full px-4 py-2 mt-1 text-gray-700 bg-white border border-gray-300 rounded-md focus:border-gold-500 focus:ring focus:ring-gold-500 focus:ring-opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:border-gold-500 font-poppins";
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6 font-poppins">
|
||||
{/* Example Fields */}
|
||||
<div>
|
||||
<label htmlFor="name" className="text-gray-700 dark:text-gray-200">
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
className={inputBaseStyle}
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="text-gray-700 dark:text-gray-200">
|
||||
Email Address *
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className={inputBaseStyle}
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="phone" className="text-gray-700 dark:text-gray-200">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
className={inputBaseStyle}
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="linkedin" className="text-gray-700 dark:text-gray-200">
|
||||
LinkedIn Profile URL
|
||||
</label>
|
||||
<input
|
||||
id="linkedin"
|
||||
name="linkedin"
|
||||
type="url"
|
||||
className={inputBaseStyle}
|
||||
value={formData.linkedin}
|
||||
onChange={handleInputChange}
|
||||
placeholder="https://linkedin.com/in/yourprofile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* --- Styled File Upload Area --- */}
|
||||
<div>
|
||||
<label className="text-gray-700 dark:text-gray-200 block mb-2">
|
||||
Resume/CV *
|
||||
</label>
|
||||
<div
|
||||
className={`relative flex flex-col items-center justify-center w-full p-6 transition-colors duration-300 ease-in-out border-2 border-dashed rounded-lg cursor-pointer hover:border-gold-500/80 ${
|
||||
isDragging
|
||||
? "border-gold-500 bg-gold-50"
|
||||
: "border-gray-300 hover:bg-gray-50"
|
||||
}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => document.getElementById("resume-upload")?.click()} // Trigger hidden input
|
||||
>
|
||||
{/* Hidden Actual Input */}
|
||||
<input
|
||||
id="resume-upload"
|
||||
name="resume"
|
||||
type="file"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
onChange={handleFileChange}
|
||||
accept=".pdf,.doc,.docx,.txt" // Specify accepted file types
|
||||
aria-hidden="true" // Hide from accessibility tree as visual element handles interaction
|
||||
/>
|
||||
|
||||
{/* Visual Cue */}
|
||||
<FaUpload
|
||||
className={`w-8 h-8 mb-3 ${
|
||||
isDragging ? "text-gold-600" : "text-gray-400"
|
||||
}`}
|
||||
style={{ color: isDragging ? "#c8a93d" : undefined }}
|
||||
/>
|
||||
<p
|
||||
className={`text-sm font-semibold text-center ${
|
||||
isDragging ? "text-gold-700" : "text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{isDragging ? "Drop file here" : "Drag & drop your file here"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
or click to browse
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">(PDF, DOC, DOCX, TXT)</p>
|
||||
</div>
|
||||
|
||||
{/* Display Selected File */}
|
||||
{resumeFile && (
|
||||
<div className="mt-3 flex items-center justify-between px-3 py-2 text-sm text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaFileAlt className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium truncate max-w-[200px] sm:max-w-xs">
|
||||
{resumeFile.name}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({(resumeFile.size / 1024).toFixed(1)} KB)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={removeFile}
|
||||
className="text-red-500 hover:text-red-700 focus:outline-none"
|
||||
aria-label="Remove file"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* --- End File Upload Area --- */}
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="coverLetter"
|
||||
className="text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
Cover Letter (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="coverLetter"
|
||||
name="coverLetter"
|
||||
rows={4}
|
||||
className={`${inputBaseStyle} resize-vertical`}
|
||||
value={formData.coverLetter}
|
||||
onChange={handleInputChange}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{/* Submit Button & Status */}
|
||||
<div className="flex flex-col items-center space-y-3 pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
isSubmitting || !formData.name || !formData.email || !resumeFile
|
||||
} // Basic validation example
|
||||
className="w-full md:w-auto px-8 py-3 font-bold text-white bg-gray-800 rounded-md hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-300"
|
||||
>
|
||||
{isSubmitting ? "Submitting..." : "Submit Application"}
|
||||
</button>
|
||||
|
||||
{submitStatus === "success" && (
|
||||
<p className="text-sm text-green-600">
|
||||
Application submitted successfully!
|
||||
</p>
|
||||
)}
|
||||
{submitStatus === "error" && (
|
||||
<p className="text-sm text-red-600">
|
||||
Submission failed. Please try again.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -20,7 +20,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
}) => {
|
||||
// Base styles including focus ring using semantic vars
|
||||
const baseStyle =
|
||||
"inline-flex items-center justify-center rounded-lg font-semibold transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
|
||||
"inline-flex items-center justify-center rounded-lg font-semibold transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background cursor-pointer disabled:pointer-events-none disabled:opacity-50 text-sm";
|
||||
|
||||
// Variant styles using semantic vars (Tailwind classes generated via @theme)
|
||||
const variantStyles = {
|
||||
|
||||
19
components/ui/CustomButton.tsx
Normal file
19
components/ui/CustomButton.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
const CustomButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
CustomButton.displayName = "CustomButton";
|
||||
|
||||
export { CustomButton };
|
||||
19
components/ui/CustomInput.tsx
Normal file
19
components/ui/CustomInput.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
type CustomInputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={`flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
CustomInput.displayName = "CustomInput";
|
||||
|
||||
export { CustomInput };
|
||||
18
components/ui/CustomLabel.tsx
Normal file
18
components/ui/CustomLabel.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
type CustomLabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
|
||||
|
||||
const CustomLabel = React.forwardRef<HTMLLabelElement, CustomLabelProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<label
|
||||
ref={ref}
|
||||
className={`text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
CustomLabel.displayName = "CustomLabel";
|
||||
|
||||
export { CustomLabel };
|
||||
19
components/ui/CustomTextarea.tsx
Normal file
19
components/ui/CustomTextarea.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
type CustomTextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
const CustomTextarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
CustomTextareaProps
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={`flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CustomTextarea.displayName = "CustomTextarea";
|
||||
|
||||
export { CustomTextarea };
|
||||
Reference in New Issue
Block a user