Files
GNX-WEB/gnx-react/components/pages/career/JobSingle.tsx
Iliyan Angelov f962401565 update
2025-10-10 02:01:46 +03:00

682 lines
28 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { JobPosition } from "@/lib/api/careerService";
import JobApplicationForm from "./JobApplicationForm";
interface JobSingleProps {
job: JobPosition;
}
const JobSingle = ({ job }: JobSingleProps) => {
const [showApplicationForm, setShowApplicationForm] = useState(false);
// Prevent body scroll when modal is open
useEffect(() => {
if (showApplicationForm) {
// Get scrollbar width to prevent layout shift
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
// Save current scroll position
const scrollY = window.scrollY;
// Prevent background scroll
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollY}px`;
document.body.style.left = '0';
document.body.style.right = '0';
document.body.style.overflow = 'hidden';
if (scrollbarWidth > 0) {
document.body.style.paddingRight = `${scrollbarWidth}px`;
}
} else {
// Get the scroll position from body top
const scrollY = parseInt(document.body.style.top || '0') * -1;
// Restore scroll
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.right = '';
document.body.style.overflow = '';
document.body.style.paddingRight = '';
// Restore scroll position
window.scrollTo(0, scrollY);
}
// Cleanup on unmount
return () => {
const scrollY = parseInt(document.body.style.top || '0') * -1;
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.right = '';
document.body.style.overflow = '';
document.body.style.paddingRight = '';
if (scrollY > 0) {
window.scrollTo(0, scrollY);
}
};
}, [showApplicationForm]);
const formatSalary = () => {
if (job.salary_min && job.salary_max) {
return `${job.salary_currency} ${job.salary_min}-${job.salary_max} ${job.salary_period}`;
} else if (job.salary_min) {
return `From ${job.salary_currency} ${job.salary_min} ${job.salary_period}`;
} else if (job.salary_max) {
return `Up to ${job.salary_currency} ${job.salary_max} ${job.salary_period}`;
}
return "Competitive";
};
const scrollToForm = () => {
setShowApplicationForm(true);
setTimeout(() => {
const formElement = document.getElementById('application-form');
if (formElement) {
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
};
return (
<>
{/* Job Header Banner */}
<section className="job-header pt-80 pt-md-100 pt-lg-120 pb-60 pb-md-70 pb-lg-80" style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
position: 'relative'
}}>
<div className="container">
<div className="row">
<div className="col-12">
<div className="job-header-content" style={{ color: 'white' }}>
<div className="mb-12 mb-md-16">
<span className="badge" style={{
backgroundColor: 'rgba(255,255,255,0.2)',
color: 'white',
padding: '6px 12px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: '500',
textTransform: 'none',
letterSpacing: '1px',
display: 'inline-block'
}}>
{job.department || 'Career Opportunity'}
</span>
</div>
<h1 className="fw-7 mb-16 mb-md-20 mb-lg-24" style={{
fontSize: 'clamp(1.75rem, 5vw, 3.5rem)',
lineHeight: '1.2',
color: 'white'
}}>
{job.title}
</h1>
<div className="job-meta d-flex flex-wrap" style={{
fontSize: 'clamp(13px, 2vw, 16px)',
gap: 'clamp(12px, 2vw, 16px)'
}}>
<div className="meta-item d-flex align-items-center">
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>location_on</span>
<span>{job.location}</span>
</div>
<div className="meta-item d-flex align-items-center">
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>work</span>
<span className="d-none d-sm-inline">{job.employment_type.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}</span>
<span className="d-sm-none">
{job.employment_type.split('-')[0].charAt(0).toUpperCase() + job.employment_type.split('-')[0].slice(1)}
</span>
</div>
<div className="meta-item d-flex align-items-center">
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>group</span>
<span className="d-none d-sm-inline">{job.open_positions} {job.open_positions === 1 ? 'Position' : 'Positions'}</span>
<span className="d-sm-none">{job.open_positions} {job.open_positions === 1 ? 'Pos' : 'Pos'}</span>
</div>
{job.experience_required && (
<div className="meta-item d-flex align-items-center d-none d-md-flex">
<span className="material-symbols-outlined me-2">school</span>
<span>{job.experience_required}</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</section>
{/* Job Content Section */}
<section className="job-single pb-80 pb-md-100 pb-lg-120 sticky-wrapper" style={{ marginTop: 'clamp(-30px, -5vw, -40px)' }}>
<div className="container">
<div className="row vertical-column-gap">
<div className="col-12 col-lg-8 mb-4 mb-lg-0">
<div className="j-d-content" style={{
backgroundColor: 'white',
borderRadius: 'clamp(8px, 2vw, 12px)',
padding: 'clamp(20px, 4vw, 40px)',
boxShadow: '0 10px 40px rgba(0,0,0,0.08)'
}}>
<div className="intro" style={{
borderBottom: '2px solid #f0f0f0',
paddingBottom: 'clamp(20px, 3vw, 30px)',
marginBottom: 'clamp(20px, 3vw, 30px)'
}}>
<h3 className="fw-6 mb-12 mb-md-16 text-secondary" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>
About This Position
</h3>
{job.short_description && (
<p style={{
color: '#666',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
{job.short_description}
</p>
)}
</div>
{job.about_role && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(22px, 4vw, 28px)'
}}>info</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
About This Role
</h4>
</div>
<p style={{
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>{job.about_role}</p>
</div>
)}
{job.requirements && job.requirements.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(22px, 4vw, 28px)'
}}>task_alt</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
Requirements
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.requirements.map((req, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{req}
</li>
))}
</ul>
</div>
)}
{job.responsibilities && job.responsibilities.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>assignment</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
Key Responsibilities
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.responsibilities.map((resp, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{resp}
</li>
))}
</ul>
</div>
)}
{job.qualifications && job.qualifications.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>workspace_premium</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
Qualifications
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.qualifications.map((qual, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{qual}
</li>
))}
</ul>
</div>
)}
{job.bonus_points && job.bonus_points.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>stars</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
Nice to Have
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.bonus_points.map((bonus, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{bonus}
</li>
))}
</ul>
</div>
)}
{job.benefits && job.benefits.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>card_giftcard</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
What We Offer
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.benefits.map((benefit, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{benefit}
</li>
))}
</ul>
</div>
)}
</div>
</div>
<div className="col-12 col-lg-4">
<div className="j-d-sidebar" style={{
backgroundColor: 'white',
borderRadius: 'clamp(8px, 2vw, 12px)',
padding: 'clamp(20px, 4vw, 30px)',
boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
position: 'sticky',
top: '20px'
}}>
<div className="intro mb-20 mb-md-30" style={{
borderBottom: '2px solid #f0f0f0',
paddingBottom: 'clamp(16px, 3vw, 20px)'
}}>
<span className="text-uppercase" style={{
color: '#667eea',
fontSize: 'clamp(11px, 2vw, 12px)',
fontWeight: '600',
letterSpacing: '2px'
}}>
JOB DETAILS
</span>
</div>
<div className="content">
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>payments</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Salary Range</p>
</div>
<p className="fw-5 mb-0" style={{
color: '#667eea',
fontSize: 'clamp(16px, 3vw, 18px)'
}}>
{formatSalary()}
</p>
{job.salary_additional && (
<p className="mt-6 mt-md-8" style={{
color: '#666',
fontSize: 'clamp(12px, 2vw, 14px)'
}}>
{job.salary_additional}
</p>
)}
</div>
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>work</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Employment Type</p>
</div>
<p style={{
color: '#666',
fontSize: 'clamp(13px, 2vw, 14px)'
}}>
{job.employment_type.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</p>
</div>
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>location_on</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Location</p>
</div>
<p style={{
color: '#666',
fontSize: 'clamp(13px, 2vw, 14px)'
}}>{job.location}</p>
</div>
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>event</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Start Date</p>
</div>
<p style={{
color: '#666',
fontSize: 'clamp(13px, 2vw, 14px)'
}}>{job.start_date}</p>
</div>
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>groups</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Openings</p>
</div>
<p style={{
color: '#666',
fontSize: 'clamp(13px, 2vw, 14px)'
}}>
{job.open_positions} {job.open_positions === 1 ? 'Position' : 'Positions'} Available
</p>
</div>
</div>
<div className="cta mt-20 mt-md-30">
<button
onClick={scrollToForm}
className="btn w-100 apply-btn"
style={{
backgroundColor: 'white',
color: '#333',
border: '2px solid #667eea',
padding: 'clamp(12px, 2vw, 15px) clamp(20px, 4vw, 30px)',
fontSize: 'clamp(14px, 2vw, 16px)',
fontWeight: '600',
borderRadius: 'clamp(6px, 1.5vw, 8px)',
transition: 'all 0.3s ease',
cursor: 'pointer'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#FFD700';
e.currentTarget.style.borderColor = '#FFD700';
e.currentTarget.style.color = '#333';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'white';
e.currentTarget.style.borderColor = '#667eea';
e.currentTarget.style.color = '#333';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<span className="d-none d-sm-inline">Apply for This Position</span>
<span className="d-sm-none">Apply Now</span>
</button>
<p className="text-center mt-12 mt-md-16" style={{
color: '#999',
fontSize: 'clamp(11px, 2vw, 13px)'
}}>
<span className="d-none d-sm-inline">Application takes ~5 minutes</span>
<span className="d-sm-none">~5 min</span>
</p>
<a
href="/career"
className="btn w-100 mt-12 mt-md-16"
style={{
backgroundColor: 'transparent',
color: '#667eea',
border: '2px solid #e0e0e0',
padding: 'clamp(10px, 2vw, 12px) clamp(20px, 4vw, 30px)',
fontSize: 'clamp(13px, 2vw, 14px)',
fontWeight: '500',
borderRadius: 'clamp(6px, 1.5vw, 8px)',
transition: 'all 0.3s ease',
cursor: 'pointer',
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 'clamp(6px, 1.5vw, 8px)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f5f5f5';
e.currentTarget.style.borderColor = '#667eea';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.borderColor = '#e0e0e0';
}}
>
<span className="material-symbols-outlined" style={{ fontSize: 'clamp(16px, 3vw, 18px)' }}>arrow_back</span>
<span className="d-none d-sm-inline">Back to Career Page</span>
<span className="d-sm-none">Back</span>
</a>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Application Form Modal/Popup */}
{showApplicationForm && (
<>
{/* Backdrop Overlay */}
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
zIndex: 9998,
animation: 'fadeIn 0.3s ease-in-out'
}}
onClick={() => setShowApplicationForm(false)}
aria-hidden="true"
/>
{/* Modal Container */}
<div
role="dialog"
aria-modal="true"
aria-labelledby="application-form-title"
tabIndex={-1}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 9999,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 'clamp(10px, 2vw, 20px)',
overflow: 'hidden',
animation: 'fadeIn 0.3s ease-in-out'
}}
onClick={(e) => {
// Close when clicking the container background
if (e.target === e.currentTarget) {
setShowApplicationForm(false);
}
}}
onKeyDown={(e) => {
// Close on ESC key
if (e.key === 'Escape') {
setShowApplicationForm(false);
}
}}
ref={(el) => {
if (el) {
setTimeout(() => el.focus(), 100);
}
}}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '16px',
width: '100%',
maxWidth: '900px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
position: 'relative',
animation: 'slideUp 0.3s ease-out',
outline: 'none',
overflow: 'hidden',
touchAction: 'none'
}}
onClick={(e) => e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
<JobApplicationForm job={job} onClose={() => setShowApplicationForm(false)} />
</div>
</div>
{/* Animation Styles */}
<style>{`
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}</style>
</>
)}
</>
);
};
export default JobSingle;