updaetd
This commit is contained in:
parent
5959b20dab
commit
763c0df3c1
@ -33,7 +33,7 @@ export default function CareersPage() {
|
||||
<AboutThree />
|
||||
<WhyChooseUs />
|
||||
<ProjectsSection />
|
||||
<CareersForm />
|
||||
{/* <CareersForm /> */}
|
||||
</main>
|
||||
<Footer1 />
|
||||
</>
|
||||
|
||||
1162
src/app/globals.css
1162
src/app/globals.css
File diff suppressed because it is too large
Load Diff
361
src/components/careers/CareersContactPopup.tsx
Normal file
361
src/components/careers/CareersContactPopup.tsx
Normal file
@ -0,0 +1,361 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import axios from "axios";
|
||||
|
||||
interface CareersContactPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
defaultPosition?: string;
|
||||
}
|
||||
|
||||
const positions = [
|
||||
{ value: "Frontend Developer", label: "Website Development" },
|
||||
{ value: "Backend Developer", label: "Social Media Marketing" },
|
||||
{ value: "Full Stack Developer", label: "Search Engine Optimization" },
|
||||
{ value: "UI/UX Designer", label: "Mobile App Development" },
|
||||
{ value: "SEO Specialist", label: "UI/UX Designing" },
|
||||
{ value: "Digital Marketing Executive", label: "Graphic Designing" },
|
||||
{ value: "Other", label: "Other" },
|
||||
];
|
||||
|
||||
const CareersContactPopup: React.FC<CareersContactPopupProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
defaultPosition = "",
|
||||
}) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
position: defaultPosition,
|
||||
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 fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Sync position when defaultPosition changes (new card clicked)
|
||||
useEffect(() => {
|
||||
setFormData((prev) => ({ ...prev, position: defaultPosition }));
|
||||
}, [defaultPosition]);
|
||||
|
||||
// Close on ESC
|
||||
useEffect(() => {
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
if (isOpen) {
|
||||
window.addEventListener("keydown", handleEsc);
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleEsc);
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
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];
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
setFormErrors((prev: any) => ({ ...prev, resume: "File size should be less than 5MB." }));
|
||||
setResumeFile(null);
|
||||
setResumeBase64(null);
|
||||
return;
|
||||
}
|
||||
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 }));
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
setResumeBase64(reader.result as string);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleCaptchaChange = (token: string | null) => {
|
||||
setCaptchaToken(token);
|
||||
};
|
||||
|
||||
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.";
|
||||
// Resume is OPTIONAL — no validation
|
||||
|
||||
setFormErrors(errors);
|
||||
if (Object.keys(errors).length > 0) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const emailData: any = {
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
position: formData.position,
|
||||
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 (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: defaultPosition, message: "" });
|
||||
setResumeFile(null);
|
||||
setResumeBase64(null);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
setCaptchaToken(null);
|
||||
setFormErrors({});
|
||||
} catch (error) {
|
||||
console.error("Error sending application:", error);
|
||||
setAlert({
|
||||
show: true,
|
||||
type: "danger",
|
||||
message: "Failed to submit application. Please try again later.",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-hide alert
|
||||
useEffect(() => {
|
||||
if (alert.show) {
|
||||
const t = setTimeout(() => setAlert((p) => ({ ...p, show: false })), 5000);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, [alert.show]);
|
||||
|
||||
const handleReset = () => {
|
||||
setFormData({ name: "", email: "", phone: "", position: defaultPosition, message: "" });
|
||||
setResumeFile(null);
|
||||
setResumeBase64(null);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
setCaptchaToken(null);
|
||||
setFormErrors({});
|
||||
setAlert({ show: false, type: "", message: "" });
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
handleReset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!mounted || !isOpen) return null;
|
||||
|
||||
const modalContent = (
|
||||
<div className={`contact-popup-overlay ${isOpen ? 'active' : ''}`} onClick={handleClose}>
|
||||
<div className="contact-popup-content careers-popup-optimized" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
{/* ── CLOSE BUTTON ── */}
|
||||
<button className="close-btn" onClick={handleClose} aria-label="Close popup" suppressHydrationWarning>
|
||||
<i className="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
|
||||
{/* ── FORM PANEL ── */}
|
||||
<div className="form-panel">
|
||||
<div className="form-header">
|
||||
<h3 className="form-title">Apply Now</h3>
|
||||
</div>
|
||||
|
||||
{alert.show && (
|
||||
<div className={`cp-alert cp-alert--${alert.type}`}>
|
||||
<i className={`fa-solid ${alert.type === "success" ? "fa-circle-check" : "fa-circle-xmark"}`}></i>
|
||||
{alert.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} noValidate className="contact-form">
|
||||
<div className="row g-3">
|
||||
<div className="col-sm-6">
|
||||
<label className="form-label-custom">Full Name <span>*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Full Name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className={`form-input ${formErrors.name ? "input-error" : ""}`}
|
||||
disabled={isSubmitting}
|
||||
suppressHydrationWarning
|
||||
/>
|
||||
{formErrors.name && <small className="text-danger">{formErrors.name}</small>}
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<label className="form-label-custom">Email Address <span>*</span></label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className={`form-input ${formErrors.email ? "input-error" : ""}`}
|
||||
disabled={isSubmitting}
|
||||
suppressHydrationWarning
|
||||
/>
|
||||
{formErrors.email && <small className="text-danger">{formErrors.email}</small>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row g-3 mt-1">
|
||||
<div className="col-sm-6">
|
||||
<label className="form-label-custom">Phone Number <span>*</span></label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
placeholder="Phone Number"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
className={`form-input ${formErrors.phone ? "input-error" : ""}`}
|
||||
disabled={isSubmitting}
|
||||
suppressHydrationWarning
|
||||
/>
|
||||
{formErrors.phone && <small className="text-danger">{formErrors.phone}</small>}
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<label className="form-label-custom">Select Position <span>*</span></label>
|
||||
<select
|
||||
name="position"
|
||||
value={formData.position}
|
||||
onChange={handleChange}
|
||||
className={`form-input form-select ${formErrors.position ? "input-error" : ""}`}
|
||||
disabled={isSubmitting}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<option value="">Select Position</option>
|
||||
{positions
|
||||
.filter(pos => !defaultPosition || pos.value === defaultPosition)
|
||||
.map((pos) => (
|
||||
<option key={pos.value} value={pos.value}>
|
||||
{pos.label}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
{formErrors.position && <small className="text-danger">{formErrors.position}</small>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-field-wrapper mt-3">
|
||||
<label className="form-label-custom">Upload Resume (PDF, DOC, DOCX) <span className="optional-label">– Optional</span></label>
|
||||
<div className="file-upload-styled">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
id="resume-upload-popup"
|
||||
type="file"
|
||||
name="resume"
|
||||
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
onChange={handleFileChange}
|
||||
className="file-hidden-input"
|
||||
disabled={isSubmitting}
|
||||
suppressHydrationWarning
|
||||
/>
|
||||
<label htmlFor="resume-upload-popup" className="file-upload-label">
|
||||
<div className="file-content">
|
||||
<i className="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span className="file-name-text">
|
||||
{resumeFile ? resumeFile.name : "Choose your resume"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="file-browse-btn">Browse</div>
|
||||
</label>
|
||||
</div>
|
||||
{formErrors.resume && <small className="text-danger">{formErrors.resume}</small>}
|
||||
</div>
|
||||
|
||||
<div className="form-field-wrapper mt-3">
|
||||
<label className="form-label-custom">Cover Letter / Message <span className="optional-label">– Optional</span></label>
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder="Write your message here..."
|
||||
rows={2}
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
className="form-textarea"
|
||||
disabled={isSubmitting}
|
||||
suppressHydrationWarning
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 g-recaptcha-container text-center">
|
||||
<ReCAPTCHA
|
||||
sitekey="6LekfpwrAAAAAOTwuP1d2gg-Fv9UEsAjE2gjOQJl"
|
||||
onChange={handleCaptchaChange}
|
||||
/>
|
||||
{formErrors.captcha && <small className="text-danger d-block mt-1">{formErrors.captcha}</small>}
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="vl-btn1 submit-btn w-100"
|
||||
disabled={isSubmitting}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<span>{isSubmitting ? "Submitting Application..." : "Submit Application"}</span>
|
||||
{!isSubmitting && <i className="fa-solid fa-arrow-right"></i>}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return typeof document !== "undefined"
|
||||
? createPortal(modalContent, document.body)
|
||||
: null;
|
||||
};
|
||||
|
||||
export default CareersContactPopup;
|
||||
@ -30,6 +30,8 @@ const projectsData = [
|
||||
}
|
||||
];
|
||||
|
||||
import CareersContactPopup from '@/components/careers/CareersContactPopup';
|
||||
|
||||
const categories = [
|
||||
{ label: "All Items", filter: "all" },
|
||||
{ label: "Website Development", filter: "web" },
|
||||
@ -43,11 +45,26 @@ const categories = [
|
||||
const ProjectsSection = () => {
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
const [activeFilter, setActiveFilter] = useState("all");
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
const [selectedPosition, setSelectedPosition] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setHasMounted(true);
|
||||
}, []);
|
||||
|
||||
const openPopup = (positionTitle: string, e?: React.MouseEvent) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
setSelectedPosition(positionTitle);
|
||||
setIsPopupOpen(true);
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
setIsPopupOpen(false);
|
||||
};
|
||||
|
||||
const filteredProjects = projectsData.filter(project =>
|
||||
activeFilter === "all" || project.category === activeFilter
|
||||
);
|
||||
@ -109,19 +126,30 @@ const ProjectsSection = () => {
|
||||
|
||||
{filteredProjects.map((project) => (
|
||||
<div key={project.id} className="project-item">
|
||||
<div className="projects-one__card">
|
||||
<div className="projects-one__card" onClick={(e) => openPopup(project.title, e)}>
|
||||
<img src={project.image} alt={project.title} />
|
||||
<div className="projects-one__card__hover d-flex">
|
||||
<div className="projects-one__card__hover-mask"></div>
|
||||
<div className="projects-one__card__hover-content-inner">
|
||||
<div className="projects-one__card__hover-text">
|
||||
<h3><Link href={project.link}>{project.title}</Link></h3>
|
||||
<h3>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => openPopup(project.title, e)}
|
||||
>
|
||||
{project.title}
|
||||
</a>
|
||||
</h3>
|
||||
<p>{project.description}</p>
|
||||
</div>
|
||||
<div className="projects-one__card__hover-item"></div>
|
||||
<Link href="/contact" className="projects-one__card__hover-link">
|
||||
<a
|
||||
href="#"
|
||||
className="projects-one__card__hover-link"
|
||||
onClick={(e) => openPopup(project.title, e)}
|
||||
>
|
||||
<i className="fa fa-arrow-right"></i>
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -136,6 +164,12 @@ const ProjectsSection = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CareersContactPopup
|
||||
isOpen={isPopupOpen}
|
||||
onClose={closePopup}
|
||||
defaultPosition={selectedPosition}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user