update
This commit is contained in:
973
gnx-react/components/pages/career/JobApplicationForm.tsx
Normal file
973
gnx-react/components/pages/career/JobApplicationForm.tsx
Normal file
@@ -0,0 +1,973 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent, ChangeEvent } from "react";
|
||||
import { JobPosition, JobApplication, careerService } from "@/lib/api/careerService";
|
||||
|
||||
interface JobApplicationFormProps {
|
||||
job: JobPosition;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const inputStyle = {
|
||||
padding: '10px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #e0e0e0',
|
||||
fontSize: '14px',
|
||||
transition: 'all 0.2s',
|
||||
width: '100%'
|
||||
};
|
||||
|
||||
const labelStyle = {
|
||||
fontWeight: '500',
|
||||
color: '#555',
|
||||
marginBottom: '6px',
|
||||
display: 'block',
|
||||
fontSize: '14px'
|
||||
};
|
||||
|
||||
const sectionStyle = {
|
||||
backgroundColor: '#ffffff',
|
||||
padding: 'clamp(16px, 3vw, 20px)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e8e8e8',
|
||||
marginBottom: '16px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)'
|
||||
};
|
||||
|
||||
const sectionHeaderStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '14px',
|
||||
paddingBottom: '10px',
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
};
|
||||
|
||||
const JobApplicationForm = ({ job, onClose }: JobApplicationFormProps) => {
|
||||
const [formData, setFormData] = useState({
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
current_position: "",
|
||||
current_company: "",
|
||||
years_of_experience: "",
|
||||
cover_letter: "",
|
||||
portfolio_url: "",
|
||||
linkedin_url: "",
|
||||
github_url: "",
|
||||
website_url: "",
|
||||
available_from: "",
|
||||
notice_period: "",
|
||||
expected_salary: "",
|
||||
salary_currency: "USD",
|
||||
consent: false,
|
||||
});
|
||||
|
||||
const [resume, setResume] = useState<File | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<{
|
||||
type: "success" | "error" | null;
|
||||
message: string;
|
||||
}>({ type: null, message: "" });
|
||||
|
||||
const handleInputChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
|
||||
if (type === "checkbox") {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
setFormData((prev) => ({ ...prev, [name]: checked }));
|
||||
} else {
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Validate file type
|
||||
const allowedTypes = [
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
];
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
setSubmitStatus({
|
||||
type: "error",
|
||||
message: "Please upload a PDF, DOC, or DOCX file",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
setSubmitStatus({
|
||||
type: "error",
|
||||
message: "Resume file size must be less than 5MB",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setResume(file);
|
||||
setSubmitStatus({ type: null, message: "" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setSubmitStatus({ type: null, message: "" });
|
||||
|
||||
// Validation
|
||||
if (!resume) {
|
||||
setSubmitStatus({
|
||||
type: "error",
|
||||
message: "Please upload your resume",
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.consent) {
|
||||
setSubmitStatus({
|
||||
type: "error",
|
||||
message: "You must consent to data processing to apply",
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const applicationData: JobApplication = {
|
||||
job: job.id,
|
||||
first_name: formData.first_name,
|
||||
last_name: formData.last_name,
|
||||
email: formData.email,
|
||||
phone: formData.phone || undefined,
|
||||
current_position: formData.current_position || undefined,
|
||||
current_company: formData.current_company || undefined,
|
||||
years_of_experience: formData.years_of_experience || undefined,
|
||||
cover_letter: formData.cover_letter || undefined,
|
||||
resume: resume,
|
||||
portfolio_url: formData.portfolio_url || undefined,
|
||||
linkedin_url: formData.linkedin_url || undefined,
|
||||
github_url: formData.github_url || undefined,
|
||||
website_url: formData.website_url || undefined,
|
||||
available_from: formData.available_from || undefined,
|
||||
notice_period: formData.notice_period || undefined,
|
||||
expected_salary: formData.expected_salary ? parseFloat(formData.expected_salary) : undefined,
|
||||
salary_currency: formData.salary_currency || undefined,
|
||||
consent: formData.consent,
|
||||
};
|
||||
|
||||
await careerService.submitApplication(applicationData);
|
||||
|
||||
setSubmitStatus({
|
||||
type: "success",
|
||||
message: "Application submitted successfully! We'll be in touch soon.",
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
current_position: "",
|
||||
current_company: "",
|
||||
years_of_experience: "",
|
||||
cover_letter: "",
|
||||
portfolio_url: "",
|
||||
linkedin_url: "",
|
||||
github_url: "",
|
||||
website_url: "",
|
||||
available_from: "",
|
||||
notice_period: "",
|
||||
expected_salary: "",
|
||||
salary_currency: "USD",
|
||||
consent: false,
|
||||
});
|
||||
setResume(null);
|
||||
|
||||
// Reset file input
|
||||
const fileInput = document.getElementById('resume') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
} catch (error) {
|
||||
setSubmitStatus({
|
||||
type: "error",
|
||||
message: error instanceof Error ? error.message : "Failed to submit application. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="job-application-form" style={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
maxHeight: '90vh'
|
||||
}}>
|
||||
{/* Header Section with Gradient */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
padding: 'clamp(24px, 4vw, 32px)',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
position: 'relative',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{/* Close Button */}
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '16px',
|
||||
right: '16px',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '24px',
|
||||
padding: '8px',
|
||||
lineHeight: '1',
|
||||
borderRadius: '50%',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s',
|
||||
zIndex: 10
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'rgba(220, 53, 69, 0.9)';
|
||||
e.currentTarget.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.2)';
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
title="Close"
|
||||
>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 'inherit' }}>close</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="intro" style={{ textAlign: 'center', color: 'white' }}>
|
||||
<div style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto 16px',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<span className="material-symbols-outlined" style={{
|
||||
fontSize: '32px',
|
||||
color: 'white'
|
||||
}}>work_outline</span>
|
||||
</div>
|
||||
<h3 id="application-form-title" className="fw-7" style={{
|
||||
color: 'white',
|
||||
fontSize: 'clamp(20px, 3vw, 24px)',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
Apply for {job.title}
|
||||
</h3>
|
||||
<p style={{
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontSize: 'clamp(13px, 2vw, 14px)',
|
||||
margin: '0'
|
||||
}}>
|
||||
Join our team and make an impact
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Content - Scrollable Area */}
|
||||
<form onSubmit={handleSubmit} style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div
|
||||
className="form-scrollable-content"
|
||||
style={{
|
||||
padding: 'clamp(20px, 4vw, 32px)',
|
||||
overflowY: 'scroll',
|
||||
overflowX: 'hidden',
|
||||
flex: '1 1 auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
touchAction: 'pan-y',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: '#667eea #f0f0f0'
|
||||
}}
|
||||
>
|
||||
|
||||
{submitStatus.type && (
|
||||
<div
|
||||
className={`alert mb-24`}
|
||||
style={{
|
||||
padding: "16px 20px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: submitStatus.type === "success" ? "#d4edda" : "#f8d7da",
|
||||
color: submitStatus.type === "success" ? "#155724" : "#721c24",
|
||||
border: `2px solid ${submitStatus.type === "success" ? "#28a745" : "#dc3545"}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>
|
||||
{submitStatus.type === "success" ? "check_circle" : "error"}
|
||||
</span>
|
||||
<span style={{ fontSize: '14px', fontWeight: '500' }}>{submitStatus.message}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Personal Information */}
|
||||
<div className="section" style={sectionStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(20px, 3vw, 22px)' }}>person</span>
|
||||
<h4 className="fw-6 mb-0" style={{ color: '#333', fontSize: 'clamp(15px, 2.5vw, 16px)' }}>Personal Information</h4>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 mb-3">
|
||||
<label htmlFor="first_name" className="form-label" style={labelStyle}>
|
||||
First Name <span style={{ color: '#dc3545' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="first_name"
|
||||
name="first_name"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
value={formData.first_name}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-3">
|
||||
<label htmlFor="last_name" className="form-label" style={labelStyle}>
|
||||
Last Name <span style={{ color: '#dc3545' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="last_name"
|
||||
name="last_name"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
value={formData.last_name}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-3">
|
||||
<label htmlFor="email" className="form-label" style={labelStyle}>
|
||||
Email Address <span style={{ color: '#dc3545' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-3">
|
||||
<label htmlFor="phone" className="form-label" style={labelStyle}>
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Professional Information */}
|
||||
<div className="section" style={sectionStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: '22px' }}>work</span>
|
||||
<h4 className="fw-6 mb-0" style={{ color: '#333', fontSize: '16px' }}>Professional Info</h4>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="current_position" className="form-label" style={labelStyle}>
|
||||
Current Position
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="current_position"
|
||||
name="current_position"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
value={formData.current_position}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="current_company" className="form-label" style={labelStyle}>
|
||||
Current Company
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="current_company"
|
||||
name="current_company"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
value={formData.current_company}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 mb-24">
|
||||
<label htmlFor="years_of_experience" className="form-label" style={labelStyle}>
|
||||
Years of Experience
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="years_of_experience"
|
||||
name="years_of_experience"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
placeholder="e.g., 3-5 years"
|
||||
value={formData.years_of_experience}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resume and Cover Letter */}
|
||||
<div className="section" style={sectionStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: '22px' }}>upload_file</span>
|
||||
<h4 className="fw-6 mb-0" style={{ color: '#333', fontSize: '16px' }}>Documents</h4>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 mb-24">
|
||||
<label htmlFor="resume" className="form-label" style={labelStyle}>
|
||||
Resume (PDF, DOC, DOCX - Max 5MB) <span style={{ color: '#dc3545' }}>*</span>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="resume"
|
||||
name="resume"
|
||||
className="form-control"
|
||||
style={{...inputStyle, padding: '10px 16px'}}
|
||||
accept=".pdf,.doc,.docx"
|
||||
onChange={handleFileChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
required
|
||||
/>
|
||||
{resume && <small style={{ color: '#28a745', fontWeight: '500', display: 'block', marginTop: '8px' }}>✓ Selected: {resume.name}</small>}
|
||||
</div>
|
||||
<div className="col-12 mb-3">
|
||||
<label htmlFor="cover_letter" className="form-label" style={labelStyle}>
|
||||
Cover Letter / Message
|
||||
</label>
|
||||
<textarea
|
||||
id="cover_letter"
|
||||
name="cover_letter"
|
||||
className="form-control"
|
||||
style={{...inputStyle, minHeight: '120px', resize: 'vertical'}}
|
||||
rows={5}
|
||||
placeholder="Tell us why you're interested in this position..."
|
||||
value={formData.cover_letter}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="section" style={sectionStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: '22px' }}>link</span>
|
||||
<h4 className="fw-6 mb-0" style={{ color: '#333', fontSize: '16px' }}>Links (Optional)</h4>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="portfolio_url" className="form-label" style={labelStyle}>
|
||||
Portfolio / Website
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="portfolio_url"
|
||||
name="portfolio_url"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
placeholder="https://"
|
||||
value={formData.portfolio_url}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="linkedin_url" className="form-label" style={labelStyle}>
|
||||
LinkedIn Profile
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="linkedin_url"
|
||||
name="linkedin_url"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
placeholder="https://linkedin.com/in/..."
|
||||
value={formData.linkedin_url}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="github_url" className="form-label" style={labelStyle}>
|
||||
GitHub Profile
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="github_url"
|
||||
name="github_url"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
placeholder="https://github.com/..."
|
||||
value={formData.github_url}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="website_url" className="form-label" style={labelStyle}>
|
||||
Personal Website
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="website_url"
|
||||
name="website_url"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
placeholder="https://"
|
||||
value={formData.website_url}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Availability & Salary */}
|
||||
<div className="section" style={sectionStyle}>
|
||||
<div style={sectionHeaderStyle}>
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: '22px' }}>event_available</span>
|
||||
<h4 className="fw-6 mb-0" style={{ color: '#333', fontSize: '16px' }}>Availability</h4>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="available_from" className="form-label" style={labelStyle}>
|
||||
Available From
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id="available_from"
|
||||
name="available_from"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
value={formData.available_from}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="notice_period" className="form-label" style={labelStyle}>
|
||||
Notice Period
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="notice_period"
|
||||
name="notice_period"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
placeholder="e.g., 2 weeks, 1 month"
|
||||
value={formData.notice_period}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="expected_salary" className="form-label" style={labelStyle}>
|
||||
Expected Salary
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="expected_salary"
|
||||
name="expected_salary"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
placeholder="Amount"
|
||||
value={formData.expected_salary}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 mb-24">
|
||||
<label htmlFor="salary_currency" className="form-label" style={labelStyle}>
|
||||
Currency
|
||||
</label>
|
||||
<select
|
||||
id="salary_currency"
|
||||
name="salary_currency"
|
||||
className="form-control"
|
||||
style={inputStyle}
|
||||
value={formData.salary_currency}
|
||||
onChange={handleInputChange}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#667eea';
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#e0e0e0';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<option value="USD">USD</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="GBP">GBP</option>
|
||||
<option value="INR">INR</option>
|
||||
<option value="AUD">AUD</option>
|
||||
<option value="CAD">CAD</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Consent */}
|
||||
<div className="section" style={{
|
||||
background: 'linear-gradient(135deg, #fff9e6 0%, #fffbf0 100%)',
|
||||
padding: 'clamp(14px, 3vw, 16px)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ffd700',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<div className="form-check d-flex align-items-start">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="consent"
|
||||
name="consent"
|
||||
className="form-check-input"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
marginTop: '2px',
|
||||
marginRight: '10px',
|
||||
cursor: 'pointer',
|
||||
flexShrink: 0,
|
||||
accentColor: '#667eea'
|
||||
}}
|
||||
checked={formData.consent}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
<label htmlFor="consent" className="form-check-label" style={{
|
||||
fontSize: 'clamp(12px, 2vw, 13px)',
|
||||
color: '#555',
|
||||
lineHeight: '1.5',
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
I consent to data processing for recruitment purposes. <span style={{ color: '#dc3545', fontWeight: '600' }}>*</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit and Cancel Buttons - Fixed Footer */}
|
||||
<div className="text-center" style={{
|
||||
paddingTop: '16px',
|
||||
paddingBottom: '16px',
|
||||
borderTop: '2px solid #e8e8e8',
|
||||
backgroundColor: 'white',
|
||||
padding: '16px clamp(20px, 4vw, 32px)',
|
||||
borderRadius: '0 0 16px 16px',
|
||||
boxShadow: '0 -4px 12px rgba(0,0,0,0.05)',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn"
|
||||
disabled={isSubmitting}
|
||||
style={{
|
||||
minWidth: "160px",
|
||||
backgroundColor: isSubmitting ? '#ccc' : 'white',
|
||||
color: '#333',
|
||||
border: '2px solid #667eea',
|
||||
padding: '12px 32px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: isSubmitting ? 'not-allowed' : 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSubmitting) {
|
||||
e.currentTarget.style.backgroundColor = '#FFD700';
|
||||
e.currentTarget.style.borderColor = '#FFD700';
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0,0,0,0.15)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isSubmitting) {
|
||||
e.currentTarget.style.backgroundColor = 'white';
|
||||
e.currentTarget.style.borderColor = '#667eea';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '20px', animation: 'spin 1s linear infinite' }}>progress_activity</span>
|
||||
Submitting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>send</span>
|
||||
Submit Application
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{onClose && !isSubmitting && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="btn"
|
||||
style={{
|
||||
minWidth: "120px",
|
||||
backgroundColor: 'transparent',
|
||||
color: '#666',
|
||||
border: '2px solid #e0e0e0',
|
||||
padding: '12px 24px',
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#f5f5f5';
|
||||
e.currentTarget.style.borderColor = '#999';
|
||||
e.currentTarget.style.color = '#333';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#e0e0e0';
|
||||
e.currentTarget.style.color = '#666';
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '18px' }}>close</span>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p style={{
|
||||
marginTop: '10px',
|
||||
color: '#999',
|
||||
fontSize: 'clamp(11px, 2vw, 12px)'
|
||||
}}>
|
||||
By submitting, you agree to our terms
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Custom scrollbar styling */
|
||||
.form-scrollable-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.form-scrollable-content::-webkit-scrollbar-track {
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form-scrollable-content::-webkit-scrollbar-thumb {
|
||||
background: #667eea;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form-scrollable-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #5568d3;
|
||||
}
|
||||
|
||||
/* Ensure smooth scrolling on touch devices */
|
||||
.form-scrollable-content {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* Better mobile input styling */
|
||||
@media (max-width: 768px) {
|
||||
.form-control {
|
||||
font-size: 16px !important; /* Prevents zoom on iOS */
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobApplicationForm;
|
||||
|
||||
@@ -1,152 +1,680 @@
|
||||
import Link from "next/link";
|
||||
"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);
|
||||
};
|
||||
|
||||
const JobSingle = () => {
|
||||
return (
|
||||
<section className="job-single fix-top pb-120 sticky-wrapper">
|
||||
<div className="container">
|
||||
<div className="row vertical-column-gap">
|
||||
<div className="col-12 col-lg-7">
|
||||
<div className="j-d-content sticky-item">
|
||||
<div className="intro">
|
||||
<h2 className="mt-8 text-secondary fw-7 title-anim mb-24">
|
||||
UI/UX Design
|
||||
</h2>
|
||||
<p>
|
||||
Position: <span className="position mb-12">(02)</span>
|
||||
</p>
|
||||
<p>
|
||||
Location: <span className="location">(Remote)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="group pt-120">
|
||||
<h4 className="mt-8 text-secondary title-anim fw-6 mb-24">
|
||||
Who we are
|
||||
</h4>
|
||||
<p className="cur-lg">
|
||||
Lorem ipsum dolor sit amet consectetur. Augue morbi sapien
|
||||
malesuada augue massa vivamus pharetra. Pellentesque velit
|
||||
lectus dui convallis posuere viverra enim mauris. Pulvinar
|
||||
quam vitae ut viverra. Vitae quis cursus magna sit amet neque
|
||||
ultricies lectus massa. Sem mauris tincidunt risus enim
|
||||
adipiscing viverra. Interdum lectus interdum diam ultricies
|
||||
molestie. In et ullamcorper semper odio enim.
|
||||
</p>
|
||||
</div>
|
||||
<div className="group mt-60">
|
||||
<h4 className="mt-8 text-secondary title-anim fw-6 mb-24">
|
||||
What you want
|
||||
</h4>
|
||||
<ul>
|
||||
<li>
|
||||
you have at least three years of commercial experience
|
||||
</li>
|
||||
<li>
|
||||
you have a strong web/UI portfolio including published
|
||||
projects
|
||||
</li>
|
||||
<li>fluent English in verbal and written communication</li>
|
||||
<li>
|
||||
you are passionate about user interface and web design
|
||||
</li>
|
||||
<li>issues are challenges not show-stoppers for you</li>
|
||||
<li>you are a trend seeker</li>
|
||||
<li>you bring a lot of attention to details</li>
|
||||
<li>
|
||||
you plan upfront, think ahead, and are ready to be surprised
|
||||
</li>
|
||||
<li>you think about the full picture</li>
|
||||
<li>
|
||||
you are familiar with any UI design tool, i.e., Sketch,
|
||||
Figma or Adobe XD
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="group mt-60">
|
||||
<h4 className="mt-8 text-secondary title-anim fw-6 mb-24">
|
||||
Who we are
|
||||
</h4>
|
||||
<p className="cur-lg">
|
||||
Lorem ipsum dolor sit amet consectetur. Augue morbi sapien
|
||||
malesuada augue massa vivamus pharetra. Pellentesque velit
|
||||
lectus dui convallis posuere viverra enim mauris. Pulvinar
|
||||
quam vitae ut viverra. Vitae quis cursus magna sit amet neque
|
||||
ultricies lectus massa. Sem mauris tincidunt risus enim
|
||||
adipiscing viverra. Interdum lectus interdum diam ultricies
|
||||
molestie. In et ullamcorper semper odio enim.
|
||||
</p>
|
||||
</div>
|
||||
<div className="group mt-60">
|
||||
<h4 className="mt-8 text-secondary title-anim fw-6 mb-24">
|
||||
Bonus points
|
||||
</h4>
|
||||
<ul>
|
||||
<li>you have at least three years</li>
|
||||
<li>you have a strong web/UI portfolio including</li>
|
||||
<li>fluent English in verbal</li>
|
||||
<li>you are passionate about user interface</li>
|
||||
<li>issues are challenges</li>
|
||||
<li>you are a seeker</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="group mt-60">
|
||||
<h4 className="mt-8 text-secondary title-anim fw-6 mb-24">
|
||||
What you get is what you see
|
||||
</h4>
|
||||
<ul>
|
||||
<li>you have at least three years</li>
|
||||
<li>you have a strong web/UI portfolio including</li>
|
||||
<li>fluent English in verbal</li>
|
||||
<li>you are passionate about user interface</li>
|
||||
<li>issues are challenges</li>
|
||||
<li>you are a seeker</li>
|
||||
<li>fluent English in verbal and written communication</li>
|
||||
<li>
|
||||
you are passionate about user interface and web design
|
||||
</li>
|
||||
<li>issues are challenges not show-stoppers for you</li>
|
||||
<li>you are a trend seeker</li>
|
||||
<li>you bring a lot of attention to details</li>
|
||||
<li>
|
||||
you plan upfront, think ahead, and are ready to be surprised
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-5 col-xxl-4 offset-xxl-1">
|
||||
<div className="j-d-sidebar sticky-item">
|
||||
<div className="intro">
|
||||
<span className="text-uppercase mt-8 text-tertiary mb-16">
|
||||
JOIN US
|
||||
</span>
|
||||
<h4 className="mt-8 fw-5 title-anim text-secondary mb-16">
|
||||
UI/UX Design
|
||||
</h4>
|
||||
<p>Full-time (40hr per week)</p>
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<p className="mt-8 fw-5 text-xl text-secondary mb-16">
|
||||
Salary-
|
||||
</p>
|
||||
<p className="mt-8 fw-5 text-xl text-secondary mb-16">
|
||||
$1500-$2000 per month
|
||||
</p>
|
||||
<p className="mt-8 fw-4 text-tertiary mb-30">
|
||||
+ VAT (B2B) + bonuses
|
||||
</p>
|
||||
<p className="mt-8 fw-4 text-tertiary mb-16">Remote / Poznań</p>
|
||||
<p className="mt-8 fw-4 text-tertiary">Start: ASAP</p>
|
||||
</div>
|
||||
<div className="cta mt-60">
|
||||
<Link href="/" className="btn">
|
||||
Apply Now
|
||||
</Link>
|
||||
<>
|
||||
{/* 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: 'uppercase',
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useJobs } from "@/lib/hooks/useCareer";
|
||||
|
||||
const OpenPosition = () => {
|
||||
const { jobs, loading, error } = useJobs();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="op-position pt-120 pb-120" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="intro mb-60">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary">
|
||||
Open Positions
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-60">
|
||||
<p className="text-center">Loading positions...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="op-position pt-120 pb-120" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="intro mb-60">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary">
|
||||
Open Positions
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-60">
|
||||
<p className="text-center text-danger">Error loading positions. Please try again later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (jobs.length === 0) {
|
||||
return (
|
||||
<section className="op-position pt-120 pb-120" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="intro mb-60">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary">
|
||||
Open Positions
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-60">
|
||||
<p className="text-center">No open positions at the moment. Please check back later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="op-position pt-120 pb-120" id="scroll-to">
|
||||
<div className="container">
|
||||
@@ -13,188 +81,36 @@ const OpenPosition = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-60">
|
||||
<div className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">01</span>
|
||||
</div>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href="job-single">UI/UX Design</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
(04 Open Roles)
|
||||
{jobs.map((job, index) => (
|
||||
<div key={job.id} className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href="job-single">
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href={`/career/${job.slug}`}>{job.title}</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
({job.open_positions.toString().padStart(2, '0')} Open {job.open_positions === 1 ? 'Role' : 'Roles'})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href={`/career/${job.slug}`}>
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">02</span>
|
||||
</div>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href="job-single">Administrative Assistant</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
(03 Open Roles)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href="job-single">
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">03</span>
|
||||
</div>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href="job-single">Software Engineer</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
(12 Open Roles)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href="job-single">
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">04</span>
|
||||
</div>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href="job-single">Data Entry Clerk</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
(01 Open Roles)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href="job-single">
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">05</span>
|
||||
</div>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href="job-single">Marketing Manager</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
(09 Open Roles)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href="job-single">
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">06</span>
|
||||
</div>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href="job-single">Executive Assistant</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
(07 Open Roles)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href="job-single">
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">07</span>
|
||||
</div>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href="job-single">Lead Product Designer</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
(03 Open Roles)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href="job-single">
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
300
gnx-react/components/pages/support/CreateTicketForm.tsx
Normal file
300
gnx-react/components/pages/support/CreateTicketForm.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { createTicket, CreateTicketData } from '@/lib/api/supportService';
|
||||
import { useTicketCategories } from '@/lib/hooks/useSupport';
|
||||
|
||||
const CreateTicketForm = () => {
|
||||
const { categories, loading: categoriesLoading } = useTicketCategories();
|
||||
|
||||
const [formData, setFormData] = useState<CreateTicketData>({
|
||||
title: '',
|
||||
description: '',
|
||||
ticket_type: 'general',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_phone: '',
|
||||
company: '',
|
||||
category: undefined
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [ticketNumber, setTicketNumber] = useState<string>('');
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: name === 'category' ? (value ? parseInt(value) : undefined) : value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setSubmitError(null);
|
||||
setSubmitSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await createTicket(formData);
|
||||
setTicketNumber(response.ticket_number);
|
||||
setSubmitSuccess(true);
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
ticket_type: 'general',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_phone: '',
|
||||
company: '',
|
||||
category: undefined
|
||||
});
|
||||
} catch (error: any) {
|
||||
setSubmitError(error.message || 'Failed to submit ticket. Please try again.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (submitSuccess) {
|
||||
return (
|
||||
<div className="ticket-success">
|
||||
<div className="success-icon">
|
||||
<i className="fa-solid fa-circle-check"></i>
|
||||
</div>
|
||||
<h3>Ticket Created Successfully!</h3>
|
||||
<p className="ticket-number">Your ticket number: <strong>{ticketNumber}</strong></p>
|
||||
<p className="ticket-info">
|
||||
We've received your support request and will respond as soon as possible.
|
||||
Please save your ticket number for future reference.
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setSubmitSuccess(false)}
|
||||
>
|
||||
Submit Another Ticket
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="create-ticket-form">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="form-header">
|
||||
<h2>Submit a Support Ticket</h2>
|
||||
<p>Fill out the form below and our team will get back to you shortly.</p>
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '8px',
|
||||
padding: '12px 16px',
|
||||
marginTop: '1rem',
|
||||
fontSize: '0.95rem',
|
||||
color: '#1e293b'
|
||||
}}>
|
||||
<strong>ℹ️ Note:</strong> Only registered email addresses can submit tickets.
|
||||
If your email is not registered, please contact support@gnxsoft.com
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submitError && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<i className="fa-solid fa-triangle-exclamation me-2"></i>
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="support-form">
|
||||
<div className="row g-4">
|
||||
{/* Personal Information */}
|
||||
<div className="col-12">
|
||||
<h4 className="form-section-title">Personal Information</h4>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="user_name">
|
||||
Full Name <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="user_name"
|
||||
name="user_name"
|
||||
value={formData.user_name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="user_email">
|
||||
Email Address <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="user_email"
|
||||
name="user_email"
|
||||
value={formData.user_email}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
placeholder="john@company.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="user_phone">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="user_phone"
|
||||
name="user_phone"
|
||||
value={formData.user_phone}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="company">Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
value={formData.company}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
placeholder="Your Company Inc."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ticket Details */}
|
||||
<div className="col-12">
|
||||
<h4 className="form-section-title">Ticket Details</h4>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="ticket_type">
|
||||
Issue Type <span className="required">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="ticket_type"
|
||||
name="ticket_type"
|
||||
value={formData.ticket_type}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
>
|
||||
<option value="general">General Inquiry</option>
|
||||
<option value="technical">Technical Issue</option>
|
||||
<option value="billing">Billing Question</option>
|
||||
<option value="feature_request">Feature Request</option>
|
||||
<option value="bug_report">Bug Report</option>
|
||||
<option value="account">Account Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="category">Category</label>
|
||||
<select
|
||||
id="category"
|
||||
name="category"
|
||||
value={formData.category || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
disabled={categoriesLoading}
|
||||
>
|
||||
<option value="">Select a category</option>
|
||||
{Array.isArray(categories) && categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form-group">
|
||||
<label htmlFor="title">
|
||||
Subject <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
placeholder="Brief description of your issue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">
|
||||
Description <span className="required">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control"
|
||||
rows={6}
|
||||
placeholder="Please provide detailed information about your issue..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Submitting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-solid fa-paper-plane me-2"></i>
|
||||
Submit Ticket
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTicketForm;
|
||||
|
||||
217
gnx-react/components/pages/support/KnowledgeBase.tsx
Normal file
217
gnx-react/components/pages/support/KnowledgeBase.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
import { useState } from 'react';
|
||||
import { useKnowledgeBaseCategories, useFeaturedArticles, useKnowledgeBaseArticles } from '@/lib/hooks/useSupport';
|
||||
import KnowledgeBaseArticleModal from './KnowledgeBaseArticleModal';
|
||||
|
||||
const KnowledgeBase = () => {
|
||||
const { categories, loading: categoriesLoading } = useKnowledgeBaseCategories();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [selectedArticleSlug, setSelectedArticleSlug] = useState<string | null>(null);
|
||||
|
||||
// Fetch all articles (for browsing and category filtering)
|
||||
const { articles: allArticles, loading: allArticlesLoading } = useKnowledgeBaseArticles();
|
||||
|
||||
// Fetch featured articles (for default view)
|
||||
const { articles: featuredArticles, loading: featuredLoading } = useFeaturedArticles();
|
||||
|
||||
// Determine which articles to display
|
||||
let displayArticles = featuredArticles;
|
||||
let isLoading = featuredLoading;
|
||||
let headerText = 'Featured Articles';
|
||||
|
||||
if (searchTerm) {
|
||||
// If searching, filter all articles by search term
|
||||
displayArticles = allArticles.filter(article =>
|
||||
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
isLoading = allArticlesLoading;
|
||||
headerText = 'Search Results';
|
||||
} else if (selectedCategory) {
|
||||
// If a category is selected, filter articles by that category
|
||||
displayArticles = allArticles.filter(article => article.category_slug === selectedCategory);
|
||||
isLoading = allArticlesLoading;
|
||||
const categoryName = categories.find(cat => cat.slug === selectedCategory)?.name || 'Category';
|
||||
headerText = `${categoryName} Articles`;
|
||||
}
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// The search is already being performed by the hook
|
||||
};
|
||||
|
||||
const filteredCategories = selectedCategory
|
||||
? categories.filter(cat => cat.slug === selectedCategory)
|
||||
: categories;
|
||||
|
||||
return (
|
||||
<div className="knowledge-base">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10">
|
||||
<div className="form-header text-center">
|
||||
<h2>Knowledge Base</h2>
|
||||
<p>Find answers to frequently asked questions and explore our documentation.</p>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<form onSubmit={handleSearch} className="kb-search-form">
|
||||
<div className="search-input-group">
|
||||
<i className="fa-solid fa-search search-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search articles, topics, or keywords..."
|
||||
className="form-control"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
className="clear-search"
|
||||
onClick={() => setSearchTerm('')}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Categories */}
|
||||
{!searchTerm && (
|
||||
<div className="kb-categories">
|
||||
<h3>Browse by Category</h3>
|
||||
{categoriesLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row g-4">
|
||||
{Array.isArray(categories) && categories.map(category => (
|
||||
<div key={category.id} className="col-md-6 col-lg-4">
|
||||
<div
|
||||
className="category-card"
|
||||
onClick={() => setSelectedCategory(category.slug)}
|
||||
style={{ borderLeftColor: category.color }}
|
||||
>
|
||||
<div
|
||||
className="category-icon"
|
||||
style={{ color: category.color }}
|
||||
>
|
||||
<i className={`fa-solid ${category.icon}`}></i>
|
||||
</div>
|
||||
<div className="category-content">
|
||||
<h4>{category.name}</h4>
|
||||
<p>{category.description}</p>
|
||||
<div className="category-meta">
|
||||
<span className="article-count">
|
||||
{category.article_count} {category.article_count === 1 ? 'article' : 'articles'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Featured/Search Results Articles */}
|
||||
<div className="kb-articles">
|
||||
<div className="articles-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h3>{headerText}</h3>
|
||||
{selectedCategory && !searchTerm && (
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
Back to All Articles
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{searchTerm && (
|
||||
<p className="search-info">
|
||||
Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : displayArticles.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<i className="fa-solid fa-search empty-icon"></i>
|
||||
<h4>No articles found</h4>
|
||||
<p>
|
||||
{searchTerm
|
||||
? `We couldn't find any articles matching "${searchTerm}". Try different keywords.`
|
||||
: 'No articles available at the moment.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="articles-list">
|
||||
{Array.isArray(displayArticles) && displayArticles.map(article => (
|
||||
<div
|
||||
key={article.id}
|
||||
className="article-item"
|
||||
onClick={() => setSelectedArticleSlug(article.slug)}
|
||||
>
|
||||
<div className="article-header">
|
||||
<h4>{article.title}</h4>
|
||||
{article.is_featured && (
|
||||
<span className="featured-badge">
|
||||
<i className="fa-solid fa-star me-1"></i>
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="article-summary">{article.summary}</p>
|
||||
<div className="article-meta">
|
||||
<span className="article-category">
|
||||
<i className="fa-solid fa-folder me-1"></i>
|
||||
{article.category_name}
|
||||
</span>
|
||||
<span className="article-stats">
|
||||
<i className="fa-solid fa-eye me-1"></i>
|
||||
{article.view_count} views
|
||||
</span>
|
||||
<span className="article-stats">
|
||||
<i className="fa-solid fa-thumbs-up me-1"></i>
|
||||
{article.helpful_count} helpful
|
||||
</span>
|
||||
</div>
|
||||
<button className="article-read-more">
|
||||
Read More <i className="fa-solid fa-arrow-right ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Modal */}
|
||||
{selectedArticleSlug && (
|
||||
<KnowledgeBaseArticleModal
|
||||
slug={selectedArticleSlug}
|
||||
onClose={() => setSelectedArticleSlug(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBase;
|
||||
|
||||
139
gnx-react/components/pages/support/KnowledgeBaseArticleModal.tsx
Normal file
139
gnx-react/components/pages/support/KnowledgeBaseArticleModal.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useKnowledgeBaseArticle } from '@/lib/hooks/useSupport';
|
||||
import { markArticleHelpful } from '@/lib/api/supportService';
|
||||
|
||||
interface KnowledgeBaseArticleModalProps {
|
||||
slug: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const KnowledgeBaseArticleModal = ({ slug, onClose }: KnowledgeBaseArticleModalProps) => {
|
||||
const { article, loading, error } = useKnowledgeBaseArticle(slug);
|
||||
const [feedbackGiven, setFeedbackGiven] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent body scroll when modal is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFeedback = async (helpful: boolean) => {
|
||||
if (!article || feedbackGiven) return;
|
||||
|
||||
try {
|
||||
await markArticleHelpful(slug, helpful);
|
||||
setFeedbackGiven(true);
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="kb-modal-overlay" onClick={onClose}>
|
||||
<div className="kb-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close modal">
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
|
||||
<div className="modal-content">
|
||||
{loading && (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p>Loading article...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-state">
|
||||
<i className="fa-solid fa-triangle-exclamation"></i>
|
||||
<h3>Error Loading Article</h3>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{article && (
|
||||
<>
|
||||
<div className="article-header">
|
||||
{article.is_featured && (
|
||||
<span className="featured-badge">
|
||||
<i className="fa-solid fa-star me-1"></i>
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
<h2>{article.title}</h2>
|
||||
<div className="article-meta">
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-folder me-1"></i>
|
||||
{article.category_name}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-calendar me-1"></i>
|
||||
{formatDate(article.published_at || article.created_at)}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-eye me-1"></i>
|
||||
{article.view_count} views
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="article-body">
|
||||
<div
|
||||
className="article-content"
|
||||
dangerouslySetInnerHTML={{ __html: article.content || article.summary }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="article-footer">
|
||||
<div className="article-feedback">
|
||||
<h4>Was this article helpful?</h4>
|
||||
{feedbackGiven ? (
|
||||
<p className="feedback-thanks">
|
||||
<i className="fa-solid fa-check-circle me-2"></i>
|
||||
Thank you for your feedback!
|
||||
</p>
|
||||
) : (
|
||||
<div className="feedback-buttons">
|
||||
<button
|
||||
className="btn btn-outline-success"
|
||||
onClick={() => handleFeedback(true)}
|
||||
>
|
||||
<i className="fa-solid fa-thumbs-up me-2"></i>
|
||||
Yes ({article.helpful_count})
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
onClick={() => handleFeedback(false)}
|
||||
>
|
||||
<i className="fa-solid fa-thumbs-down me-2"></i>
|
||||
No ({article.not_helpful_count})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseArticleModal;
|
||||
|
||||
55
gnx-react/components/pages/support/SupportCenterContent.tsx
Normal file
55
gnx-react/components/pages/support/SupportCenterContent.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
import { useState } from 'react';
|
||||
import CreateTicketForm from './CreateTicketForm';
|
||||
import KnowledgeBase from './KnowledgeBase';
|
||||
import TicketStatusCheck from './TicketStatusCheck';
|
||||
|
||||
type TabType = 'create' | 'knowledge' | 'status';
|
||||
|
||||
const SupportCenterContent = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('create');
|
||||
|
||||
return (
|
||||
<section className="support-center-content section-padding">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
{/* Tab Navigation */}
|
||||
<div className="support-tabs">
|
||||
<ul className="support-tabs__nav">
|
||||
<li className={activeTab === 'create' ? 'active' : ''}>
|
||||
<button onClick={() => setActiveTab('create')}>
|
||||
<i className="fa-solid fa-ticket me-2"></i>
|
||||
Submit a Ticket
|
||||
</button>
|
||||
</li>
|
||||
<li className={activeTab === 'knowledge' ? 'active' : ''}>
|
||||
<button onClick={() => setActiveTab('knowledge')}>
|
||||
<i className="fa-solid fa-book me-2"></i>
|
||||
Knowledge Base
|
||||
</button>
|
||||
</li>
|
||||
<li className={activeTab === 'status' ? 'active' : ''}>
|
||||
<button onClick={() => setActiveTab('status')}>
|
||||
<i className="fa-solid fa-search me-2"></i>
|
||||
Check Ticket Status
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="support-tabs__content">
|
||||
{activeTab === 'create' && <CreateTicketForm />}
|
||||
{activeTab === 'knowledge' && <KnowledgeBase />}
|
||||
{activeTab === 'status' && <TicketStatusCheck />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportCenterContent;
|
||||
|
||||
91
gnx-react/components/pages/support/SupportCenterHero.tsx
Normal file
91
gnx-react/components/pages/support/SupportCenterHero.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
const SupportCenterHero = () => {
|
||||
return (
|
||||
<section className="support-hero">
|
||||
{/* Animated Background */}
|
||||
<div className="hero-background">
|
||||
{/* Floating Support Icons */}
|
||||
<div className="floating-tech tech-1">
|
||||
<i className="fa-solid fa-headset"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-2">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-3">
|
||||
<i className="fa-solid fa-book"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-4">
|
||||
<i className="fa-solid fa-comments"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-5">
|
||||
<i className="fa-solid fa-life-ring"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-6">
|
||||
<i className="fa-solid fa-user-shield"></i>
|
||||
</div>
|
||||
|
||||
{/* Grid Pattern */}
|
||||
<div className="grid-overlay"></div>
|
||||
|
||||
{/* Animated Gradient Orbs */}
|
||||
<div className="gradient-orb orb-1"></div>
|
||||
<div className="gradient-orb orb-2"></div>
|
||||
<div className="gradient-orb orb-3"></div>
|
||||
|
||||
{/* Video Overlay */}
|
||||
<div className="video-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="support-hero__content text-center">
|
||||
<h1 className="support-hero__title">
|
||||
Support Center
|
||||
</h1>
|
||||
<p className="support-hero__subtitle">
|
||||
Get expert assistance whenever you need it. Our dedicated support team is here to help you succeed.
|
||||
</p>
|
||||
|
||||
<div className="support-hero__features">
|
||||
<div className="row g-4 justify-content-center">
|
||||
<div className="col-md-4">
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<h3>Submit Tickets</h3>
|
||||
<p>Create and track support requests</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-book"></i>
|
||||
</div>
|
||||
<h3>Knowledge Base</h3>
|
||||
<p>Find answers to common questions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-search"></i>
|
||||
</div>
|
||||
<h3>Track Status</h3>
|
||||
<p>Monitor your ticket progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportCenterHero;
|
||||
|
||||
197
gnx-react/components/pages/support/TicketStatusCheck.tsx
Normal file
197
gnx-react/components/pages/support/TicketStatusCheck.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { checkTicketStatus, SupportTicket } from '@/lib/api/supportService';
|
||||
|
||||
const TicketStatusCheck = () => {
|
||||
const [ticketNumber, setTicketNumber] = useState('');
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchError, setSearchError] = useState<string | null>(null);
|
||||
const [ticket, setTicket] = useState<SupportTicket | null>(null);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSearching(true);
|
||||
setSearchError(null);
|
||||
setTicket(null);
|
||||
|
||||
try {
|
||||
const response = await checkTicketStatus(ticketNumber);
|
||||
setTicket(response);
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Ticket not found') {
|
||||
setSearchError('Ticket not found. Please check your ticket number and try again.');
|
||||
} else {
|
||||
setSearchError('An error occurred while searching. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ticket-status-check">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-8">
|
||||
<div className="form-header text-center">
|
||||
<h2>Check Ticket Status</h2>
|
||||
<p>Enter your ticket number to view the current status and details of your support request.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="status-search-form">
|
||||
<div className="search-input-group">
|
||||
<input
|
||||
type="text"
|
||||
value={ticketNumber}
|
||||
onChange={(e) => setTicketNumber(e.target.value)}
|
||||
placeholder="Enter your ticket number (e.g., TKT-20231015-ABCDE)"
|
||||
required
|
||||
className="form-control"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={isSearching}
|
||||
>
|
||||
{isSearching ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Searching...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-solid fa-search me-2"></i>
|
||||
Check Status
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{searchError && (
|
||||
<div className="alert alert-danger mt-4" role="alert">
|
||||
<i className="fa-solid fa-triangle-exclamation me-2"></i>
|
||||
{searchError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ticket && (
|
||||
<div className="ticket-details">
|
||||
<div className="ticket-header">
|
||||
<div className="ticket-number">
|
||||
<i className="fa-solid fa-ticket me-2"></i>
|
||||
{ticket.ticket_number}
|
||||
</div>
|
||||
<div
|
||||
className="ticket-status-badge"
|
||||
style={{ backgroundColor: ticket.status_color }}
|
||||
>
|
||||
{ticket.status_name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ticket-info">
|
||||
<h3>{ticket.title}</h3>
|
||||
<div className="ticket-meta">
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-calendar me-2"></i>
|
||||
<strong>Created:</strong> {formatDate(ticket.created_at)}
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-clock me-2"></i>
|
||||
<strong>Last Updated:</strong> {formatDate(ticket.updated_at)}
|
||||
</div>
|
||||
{ticket.priority_name && (
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-flag me-2"></i>
|
||||
<strong>Priority:</strong>
|
||||
<span
|
||||
className="priority-badge ms-2"
|
||||
style={{ backgroundColor: ticket.priority_color }}
|
||||
>
|
||||
{ticket.priority_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{ticket.category_name && (
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-folder me-2"></i>
|
||||
<strong>Category:</strong> {ticket.category_name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ticket-description">
|
||||
<h4>Description</h4>
|
||||
<p>{ticket.description}</p>
|
||||
</div>
|
||||
|
||||
{ticket.messages && ticket.messages.length > 0 && (
|
||||
<div className="ticket-messages">
|
||||
<h4>Messages ({ticket.messages.length})</h4>
|
||||
<div className="messages-list">
|
||||
{ticket.messages
|
||||
.filter(msg => !msg.is_internal)
|
||||
.map((message, index) => (
|
||||
<div key={message.id} className="message-item">
|
||||
<div className="message-header">
|
||||
<div className="message-author">
|
||||
<i className="fa-solid fa-user-circle me-2"></i>
|
||||
{message.author_name || message.author_email}
|
||||
</div>
|
||||
<div className="message-date">
|
||||
{formatDate(message.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="message-content">
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ticket.activities && ticket.activities.length > 0 && (
|
||||
<div className="ticket-timeline">
|
||||
<h4>Activity Timeline</h4>
|
||||
<div className="timeline-list">
|
||||
{ticket.activities.slice(0, 5).map((activity, index) => (
|
||||
<div key={activity.id} className="timeline-item">
|
||||
<div className="timeline-icon">
|
||||
<i className="fa-solid fa-circle"></i>
|
||||
</div>
|
||||
<div className="timeline-content">
|
||||
<div className="timeline-description">
|
||||
{activity.description}
|
||||
</div>
|
||||
<div className="timeline-date">
|
||||
{formatDate(activity.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TicketStatusCheck;
|
||||
|
||||
Reference in New Issue
Block a user