updaetd
This commit is contained in:
parent
5959b20dab
commit
763c0df3c1
@ -33,7 +33,7 @@ export default function CareersPage() {
|
|||||||
<AboutThree />
|
<AboutThree />
|
||||||
<WhyChooseUs />
|
<WhyChooseUs />
|
||||||
<ProjectsSection />
|
<ProjectsSection />
|
||||||
<CareersForm />
|
{/* <CareersForm /> */}
|
||||||
</main>
|
</main>
|
||||||
<Footer1 />
|
<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 = [
|
const categories = [
|
||||||
{ label: "All Items", filter: "all" },
|
{ label: "All Items", filter: "all" },
|
||||||
{ label: "Website Development", filter: "web" },
|
{ label: "Website Development", filter: "web" },
|
||||||
@ -43,11 +45,26 @@ const categories = [
|
|||||||
const ProjectsSection = () => {
|
const ProjectsSection = () => {
|
||||||
const [hasMounted, setHasMounted] = useState(false);
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
const [activeFilter, setActiveFilter] = useState("all");
|
const [activeFilter, setActiveFilter] = useState("all");
|
||||||
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||||
|
const [selectedPosition, setSelectedPosition] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasMounted(true);
|
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 =>
|
const filteredProjects = projectsData.filter(project =>
|
||||||
activeFilter === "all" || project.category === activeFilter
|
activeFilter === "all" || project.category === activeFilter
|
||||||
);
|
);
|
||||||
@ -109,19 +126,30 @@ const ProjectsSection = () => {
|
|||||||
|
|
||||||
{filteredProjects.map((project) => (
|
{filteredProjects.map((project) => (
|
||||||
<div key={project.id} className="project-item">
|
<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} />
|
<img src={project.image} alt={project.title} />
|
||||||
<div className="projects-one__card__hover d-flex">
|
<div className="projects-one__card__hover d-flex">
|
||||||
<div className="projects-one__card__hover-mask"></div>
|
<div className="projects-one__card__hover-mask"></div>
|
||||||
<div className="projects-one__card__hover-content-inner">
|
<div className="projects-one__card__hover-content-inner">
|
||||||
<div className="projects-one__card__hover-text">
|
<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>
|
<p>{project.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="projects-one__card__hover-item"></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>
|
<i className="fa fa-arrow-right"></i>
|
||||||
</Link>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,6 +164,12 @@ const ProjectsSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CareersContactPopup
|
||||||
|
isOpen={isPopupOpen}
|
||||||
|
onClose={closePopup}
|
||||||
|
defaultPosition={selectedPosition}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user