// Careers page — job listings, per-job application form, open application
const { useState: useCareersState, useEffect: useCareersEffect, useRef: useCareersRef } = React;
// ── Paste your Google Apps Script Web App URL here after deploying
// Follow the setup guide in careers_apps_script.md to deploy your script
const CAREERS_WEBHOOK_URL = 'https://script.google.com/macros/s/AKfycbxnqVfYpcNNFlXw-40W8Z7skRuOMkn81rQlqwD3fNwFUp1H8VJu6FTdycBGGww8hpYl2g/exec';
// ── Fetch jobs from backend
// Note: Actual fetching happens in CareersPage component
// ── Hero
const CareersHero = () => (
Join the Team · AMG Careers
Build somethingthat lasts.
We don't just build landmarks — we build careers. Join a 40-year legacy of trust, craft, and community across Punjab's real estate landscape.
{/* Stats strip */}
{[
{ v: '40+', k: 'Years of Legacy' },
{ v: '10K+', k: 'Investor Families' },
{ v: '8+', k: 'Active Projects' },
].map(s => (
))}
);
// ── Stable field wrapper — defined OUTSIDE modal so React never remounts it on re-render
const CareersField = ({ label, error, children }) => (
{label}
{children}
{error && {error} }
);
// ── Application form modal — rendered via portal to escape Framer Motion transform context
function ApplicationModal({ job, onClose }) {
const [form, setForm] = useCareersState({ name: '', email: '', phone: '', message: '', openPosition: '', openDepartment: '', openLocation: '' });
const [resume, setResume] = useCareersState(null);
const [errors, setErrors] = useCareersState({});
const [status, setStatus] = useCareersState('idle'); // idle | submitting | success | error
const fileRef = useCareersRef();
const isOpen = job?.title === '__open__';
// Lock body scroll while open
useCareersEffect(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
if (window.lenis) window.lenis.stop();
return () => {
document.body.style.overflow = prev;
if (window.lenis) window.lenis.start();
};
}, []);
// ── Validators
const validateEmail = (v) => {
const s = v.trim().toLowerCase();
if (!s) return 'Email address is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(s)) return 'Enter a valid email address';
const domain = s.split('@')[1] || '';
if (!domain.includes('.')) return 'Enter a valid email address';
return null;
};
const validatePhone = (v) => {
const digits = v.replace(/\D/g, '');
if (!digits) return 'Phone number is required';
if (digits.length !== 10) return 'Enter a valid 10-digit Indian mobile number';
if (!/^[6-9]/.test(digits)) return 'Indian numbers start with 6, 7, 8 or 9';
if (/^(.)\1+$/.test(digits)) return 'Enter a real mobile number';
return null;
};
const validate = () => {
const e = {};
if (!form.name.trim() || form.name.trim().length < 2) e.name = 'Please enter your full name';
const emailErr = validateEmail(form.email);
if (emailErr) e.email = emailErr;
const phoneErr = validatePhone(form.phone);
if (phoneErr) e.phone = phoneErr;
if (isOpen) {
if (!form.openDepartment.trim()) e.openDepartment = 'Please specify a department';
if (!form.openPosition.trim()) e.openPosition = 'Please specify a position';
if (!form.openLocation.trim()) e.openLocation = 'Please specify a location';
}
if (!resume) e.resume = 'Please attach your resume (PDF or DOCX)';
return e;
};
// ── File handler — validate by extension (MIME type is unreliable on Windows)
const handleFile = (e) => {
const f = e.target.files[0];
if (!f) return;
const name = f.name.toLowerCase();
const allowed = ['.pdf', '.doc', '.docx'];
if (!allowed.some(ext => name.endsWith(ext))) {
setErrors(prev => ({ ...prev, resume: 'Only PDF or DOCX files are accepted' }));
e.target.value = ''; // reset so same file can be picked again
return;
}
if (f.size > 5 * 1024 * 1024) {
setErrors(prev => ({ ...prev, resume: 'File must be under 5 MB' }));
e.target.value = '';
return;
}
setResume(f);
setErrors(prev => { const n = {...prev}; delete n.resume; return n; });
};
// ── Strip non-digits from phone field
const handlePhone = (e) => {
const digits = e.target.value.replace(/\D/g, '').slice(0, 10);
setForm(p => ({...p, phone: digits}));
if (errors.phone) setErrors(prev => { const n={...prev}; delete n.phone; return n; });
};
// ── Convert file to base64 helper
const toBase64 = (file) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]); // strip data:...;base64, prefix
reader.onerror = reject;
reader.readAsDataURL(file);
});
const handleSubmit = async (ev) => {
ev.preventDefault();
const e = validate();
if (Object.keys(e).length) { setErrors(e); return; }
setStatus('submitting');
try {
// Convert resume to base64 so Apps Script can save it to Google Drive
let resumeBase64 = null;
if (resume) {
resumeBase64 = await toBase64(resume);
}
const payload = {
name: form.name.trim(),
email: form.email.trim().toLowerCase(),
phone: form.phone.trim(),
coverNote: form.message.trim(),
position: isOpen ? form.openPosition.trim() : (job?.title || ''),
department: isOpen ? form.openDepartment.trim() : (job?.department || '—'),
location: isOpen ? form.openLocation.trim() : (job?.location || '—'),
resumeName: resume ? resume.name : '',
resumeBase64: resumeBase64 || '',
submittedAt: new Date().toISOString(),
};
if (CAREERS_WEBHOOK_URL && CAREERS_WEBHOOK_URL !== 'PASTE_YOUR_APPS_SCRIPT_URL_HERE') {
await fetch(CAREERS_WEBHOOK_URL, {
method: 'POST',
mode: 'no-cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
setStatus('success');
} catch (err) {
setStatus('error');
}
};
const modalContent = (
{ if (e.target === e.currentTarget) onClose(); }}
>
e.stopPropagation()}
>
{/* Header */}
{isOpen ? 'Open Application' : (job?.department || 'Position')}
{isOpen ? 'Apply for Future Roles' : job?.title}
{status === 'success' ? (
Application received!
Thank you, {form.name.split(' ')[0] || 'there'} . Your application and resume have been sent to our HR team. We'll be in touch if there's a fit.
Close
) : status === 'error' ? (
Submission failed
Could not reach the server. Please email us directly at hr@amgrealty.in
setStatus('idle')} className="mt-6 btn rounded-full border border-bone-50/15 text-bone-50/60 px-6 py-3 text-[13px] hover:text-bone-50 hover:border-bone-50/30 transition">Try again
) : (
)}
);
// Portal to body — escapes Framer Motion transform stacking context
return ReactDOM.createPortal(modalContent, document.body);
}
// ── Job card
function JobCard({ job, onApply }) {
const [open, setOpen] = useCareersState(false);
return (
setOpen(o => !o)}
className="w-full text-left px-6 sm:px-8 py-6 flex items-start sm:items-center justify-between gap-4"
>
{job.title}
{job.department && {job.department} }
{job.location && {job.location} }
{job.type && {job.type} }
{open && (
{job.description && (
{job.description}
)}
{job.responsibilities && job.responsibilities.length > 0 && (
Responsibilities
{job.responsibilities.map((r, i) => (
{r}
))}
)}
{job.requirements && job.requirements.length > 0 && (
Requirements
{job.requirements.map((r, i) => (
{r}
))}
)}
onApply(job)}
className="btn rounded-full bg-bone-50 text-ink-950 px-5 py-3 text-[12px] font-medium tracking-wide hover:bg-accent-soft inline-flex items-center gap-2 transition"
>
Apply for this role
)}
);
}
// ── Open application banner
function OpenApplicationBanner({ onApply }) {
return (
Always Open · Talent Pipeline
Don't see your role?
We'd still love to meet you.
We're always looking for exceptional talent — architects, sales professionals, engineers, marketers, and more. Submit your profile and we'll reach out when the right opportunity arises.
onApply({ title: '__open__' })}
className="btn rounded-full bg-bone-50 text-ink-950 px-6 py-4 text-[13px] font-medium tracking-wide hover:bg-accent-soft inline-flex items-center justify-between transition"
>
Submit Open Application
);
}
// ── Main Careers page
function CareersPage({ setPage }) {
useReveal();
const [jobs, setJobs] = useCareersState([]);
const [loading, setLoading] = useCareersState(true);
const [applyJob, setApplyJob] = useCareersState(null);
// Fetch jobs from Google Sheets on load
useCareersEffect(() => {
fetch(CAREERS_WEBHOOK_URL)
.then(res => res.json())
.then(data => { setJobs(data); setLoading(false); })
.catch(err => { console.error('Failed to load jobs', err); setLoading(false); });
}, []);
return (
{applyJob && setApplyJob(null)}/>}
{/* Open Positions */}
Currentopportunities. >}/>
{jobs.filter(j => j.active).length} role{jobs.filter(j => j.active).length !== 1 ? 's' : ''} open
{loading ? (
Loading current openings...
) : jobs.filter(j => j.active).length === 0 ? (
No specific openings right now — but we're always growing.
Check back soon, or send an open application below.
) : (
{jobs.filter(j => j.active).map(job => (
))}
)}
);
}
window.CareersPage = CareersPage;