This commit is contained in:
Iliyan Angelov
2025-10-07 22:10:27 +03:00
parent 3f5bcfad68
commit d48c54e2c5
3221 changed files with 40187 additions and 92575 deletions

View 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;

View File

@@ -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>
</>
)}
</>
);
};

View File

@@ -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>

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;