career form updated
This commit is contained in:
parent
eb7c190e80
commit
7061fcff3e
@ -8,6 +8,7 @@ import AboutService from "@/components/services-digital-solutions/AboutService";
|
|||||||
import AboutThree from "@/components/home/AboutThree";
|
import AboutThree from "@/components/home/AboutThree";
|
||||||
import ProjectsSection from "@/components/home/ProjectsSection";
|
import ProjectsSection from "@/components/home/ProjectsSection";
|
||||||
import MetatronInitializer from "@/components/MetatronInitializer";
|
import MetatronInitializer from "@/components/MetatronInitializer";
|
||||||
|
import CareersForm from "@/components/careers/CareersForm";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Careers - Metatroncube Software Solutions | Innovative & User-Centric Tech Services in Waterloo",
|
title: "Careers - Metatroncube Software Solutions | Innovative & User-Centric Tech Services in Waterloo",
|
||||||
@ -19,7 +20,7 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function CareersPage() {
|
export default function CareersPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MetatronInitializer />
|
<MetatronInitializer />
|
||||||
<Header1 />
|
<Header1 />
|
||||||
<main>
|
<main>
|
||||||
@ -32,6 +33,7 @@ export default function CareersPage() {
|
|||||||
<AboutThree />
|
<AboutThree />
|
||||||
<WhyChooseUs />
|
<WhyChooseUs />
|
||||||
<ProjectsSection />
|
<ProjectsSection />
|
||||||
|
<CareersForm />
|
||||||
</main>
|
</main>
|
||||||
<Footer1 />
|
<Footer1 />
|
||||||
</>
|
</>
|
||||||
|
|||||||
338
src/components/careers/CareersForm.tsx
Normal file
338
src/components/careers/CareersForm.tsx
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import ReCAPTCHA from "react-google-recaptcha";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const CareersForm = () => {
|
||||||
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
position: "",
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [resumeFile, setResumeFile] = useState<File | null>(null);
|
||||||
|
const [resumeBase64, setResumeBase64] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [formErrors, setFormErrors] = useState<any>({});
|
||||||
|
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
|
||||||
|
const [alert, setAlert] = useState({ show: false, type: "", message: "" });
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
|
||||||
|
// Check file size (e.g., max 5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
setFormErrors((prev: any) => ({ ...prev, resume: "File size should be less than 5MB." }));
|
||||||
|
setResumeFile(null);
|
||||||
|
setResumeBase64(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file type
|
||||||
|
const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||||||
|
if (!allowedTypes.includes(file.type)) {
|
||||||
|
setFormErrors((prev: any) => ({ ...prev, resume: "Only PDF and Word documents are allowed." }));
|
||||||
|
setResumeFile(null);
|
||||||
|
setResumeBase64(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResumeFile(file);
|
||||||
|
setFormErrors((prev: any) => ({ ...prev, resume: undefined }));
|
||||||
|
|
||||||
|
// Convert to Base64 for email attachment
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => {
|
||||||
|
setResumeBase64(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
console.error("Error reading file:", error);
|
||||||
|
setFormErrors((prev: any) => ({ ...prev, resume: "Error reading file." }));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCaptchaChange = (token: string | null) => {
|
||||||
|
setCaptchaToken(token);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const elements = entry.target.querySelectorAll(".wow");
|
||||||
|
elements.forEach((el) => {
|
||||||
|
const htmlEl = el as HTMLElement;
|
||||||
|
const delay = htmlEl.dataset.wowDelay || "0ms";
|
||||||
|
setTimeout(() => {
|
||||||
|
htmlEl.classList.add("animated");
|
||||||
|
|
||||||
|
if (htmlEl.classList.contains("fadeInLeft")) {
|
||||||
|
htmlEl.classList.add("fadeInLeft");
|
||||||
|
} else if (htmlEl.classList.contains("fadeInRight")) {
|
||||||
|
htmlEl.classList.add("fadeInRight");
|
||||||
|
} else {
|
||||||
|
htmlEl.classList.add("fadeInUp");
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlEl.style.visibility = "visible";
|
||||||
|
}, parseInt(delay));
|
||||||
|
});
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sectionRef.current) {
|
||||||
|
observer.observe(sectionRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const errors: any = {};
|
||||||
|
if (!formData.name.trim()) errors.name = "Name is required.";
|
||||||
|
if (!formData.email.trim()) errors.email = "Email is required.";
|
||||||
|
if (!formData.phone.trim()) errors.phone = "Phone is required.";
|
||||||
|
if (!formData.position.trim()) errors.position = "Please select a position.";
|
||||||
|
// if (!captchaToken) errors.captcha = "Please verify the CAPTCHA.";
|
||||||
|
if (!resumeFile) errors.resume = "Please upload your resume.";
|
||||||
|
|
||||||
|
setFormErrors(errors);
|
||||||
|
if (Object.keys(errors).length > 0) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
const emailData: any = {
|
||||||
|
...formData,
|
||||||
|
message: `Position: ${formData.position}<br /><br />Phone: ${formData.phone}<br /><br />Message: ${formData.message || "No additional message."}`,
|
||||||
|
to: "info@metatroncubesolutions.com",
|
||||||
|
senderName: "Metatroncube Careers Application",
|
||||||
|
recaptchaToken: captchaToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the backend endpoint supports attachments via Nodemailer's "attachments" array matching to data URI paths
|
||||||
|
if (resumeBase64 && resumeFile) {
|
||||||
|
emailData.attachments = [
|
||||||
|
{
|
||||||
|
filename: resumeFile.name,
|
||||||
|
path: resumeBase64
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.post("https://mailserver.metatronnest.com/send", emailData, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
setAlert({
|
||||||
|
show: true,
|
||||||
|
type: "success",
|
||||||
|
message: res?.data?.message || "Application submitted successfully!",
|
||||||
|
});
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
position: "",
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
|
setResumeFile(null);
|
||||||
|
setResumeBase64(null);
|
||||||
|
// Reset file input value manually
|
||||||
|
const fileInput = document.getElementById("resume-upload") as HTMLInputElement;
|
||||||
|
if (fileInput) fileInput.value = "";
|
||||||
|
|
||||||
|
setCaptchaToken(null);
|
||||||
|
setFormErrors({});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error sending email:", error);
|
||||||
|
setAlert({
|
||||||
|
show: true,
|
||||||
|
type: "danger",
|
||||||
|
message: "Failed to submit application. Please try again later.",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (alert.show) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setAlert((prev) => ({ ...prev, show: false }));
|
||||||
|
}, 5000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [alert.show]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={sectionRef} className="contact-one section-space-bottom" id="careers-form">
|
||||||
|
|
||||||
|
<div className="container contact-one__container wow fadeInUp" data-wow-delay="100ms">
|
||||||
|
<div
|
||||||
|
className="contact-one__wrapper"
|
||||||
|
style={{ backgroundImage: "url(/assets/images/about/7/element-bottom-right.webp)" }}
|
||||||
|
>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-6">
|
||||||
|
<div className="contact-one__image-two">
|
||||||
|
<img src="/assets/images/about/7/4-left-img.webp" alt="Career Growth" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-6">
|
||||||
|
<div className="contact-one__content">
|
||||||
|
<div className="sec-title text-left">
|
||||||
|
<h6 className="sec-title__tagline">
|
||||||
|
<span className="sec-title__tagline__left"></span>
|
||||||
|
Join Our Team
|
||||||
|
<span className="sec-title__tagline__right"></span>
|
||||||
|
</h6>
|
||||||
|
<h3 className="sec-title__title" style={{ color: "#fff" }}>
|
||||||
|
Apply for your dream position today.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{alert.show && (
|
||||||
|
<div className={`alert alert-${alert.type === 'danger' ? 'danger' : 'success'} mb-4`} style={{ padding: '15px', color: '#fff', backgroundColor: alert.type === 'danger' ? 'rgba(255, 107, 107, 0.2)' : 'rgba(76, 175, 80, 0.2)', border: alert.type === 'danger' ? '1px solid #ff6b6b' : '1px solid #4CAF50', borderRadius: '8px' }}>
|
||||||
|
{alert.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form className="contact-one__form form-one" onSubmit={handleSubmit}>
|
||||||
|
<div className="form-one__group">
|
||||||
|
<div className="mb-20">
|
||||||
|
<label className="form-label">Full Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
placeholder="Full Name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="form-input"
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
/>
|
||||||
|
{formErrors.name && <small className="text-danger">{formErrors.name}</small>}
|
||||||
|
</div>
|
||||||
|
<div className="mb-20">
|
||||||
|
<label className="form-label">Email Address *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="Email Address"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="form-input"
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
/>
|
||||||
|
{formErrors.email && <small className="text-danger">{formErrors.email}</small>}
|
||||||
|
</div>
|
||||||
|
<div className="mb-20">
|
||||||
|
<label className="form-label">Phone Number *</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
placeholder="Phone Number"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="form-input"
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
/>
|
||||||
|
{formErrors.phone && <small className="text-danger">{formErrors.phone}</small>}
|
||||||
|
</div>
|
||||||
|
<div className="mb-20">
|
||||||
|
<label className="form-label">Select Position *</label>
|
||||||
|
<select
|
||||||
|
name="position"
|
||||||
|
value={formData.position}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="form-select-custom"
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
>
|
||||||
|
<option value="">Select Position</option>
|
||||||
|
<option value="Frontend Developer">Website Development</option>
|
||||||
|
<option value="Backend Developer">Social Media Marketing</option>
|
||||||
|
<option value="Full Stack Developer">Search Engine Optimization</option>
|
||||||
|
<option value="UI/UX Designer">Mobile App Development</option>
|
||||||
|
<option value="SEO Specialist">UI/UX Designing</option>
|
||||||
|
<option value="Digital Marketing Executive">Graphic Designing</option>
|
||||||
|
<option value="Other">Other</option>
|
||||||
|
</select>
|
||||||
|
{formErrors.position && <small className="text-danger">{formErrors.position}</small>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-one__control--full mb-20 mt-2">
|
||||||
|
<label className="form-label">Upload Resume (PDF, DOC, DOCX) *</label>
|
||||||
|
<input
|
||||||
|
id="resume-upload"
|
||||||
|
type="file"
|
||||||
|
name="resume"
|
||||||
|
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="form-input p-2"
|
||||||
|
style={{ background: 'transparent', color: '#fff', border: '1px solid rgba(255, 255, 255, 0.2)' }}
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
/>
|
||||||
|
{formErrors.resume && <small className="text-danger">{formErrors.resume}</small>}
|
||||||
|
{resumeFile && !formErrors.resume && <small className="text-success d-block mt-1">File selected: {resumeFile.name}</small>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-one__control--full mb-20">
|
||||||
|
<label className="form-label">Cover Letter / Message (Optional)</label>
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
placeholder="Write your message here..."
|
||||||
|
rows={3}
|
||||||
|
value={formData.message}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="form-textarea"
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="form-one__control--full mb-3 mt-3">
|
||||||
|
<ReCAPTCHA
|
||||||
|
sitekey="6LekfpwrAAAAAOTwuP1d2gg-Fv9UEsAjE2gjOQJl"
|
||||||
|
onChange={handleCaptchaChange}
|
||||||
|
/>
|
||||||
|
{formErrors.captcha && <small className="text-danger">{formErrors.captcha}</small>}
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="form-one__control--full">
|
||||||
|
<button type="submit" className="submit-btn tolak-btn w-100" disabled={isSubmitting}>
|
||||||
|
<b>{isSubmitting ? "SUBMITTING..." : "SUBMIT APPLICATION"}</b>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CareersForm;
|
||||||
Loading…
x
Reference in New Issue
Block a user