Initial commit: AI Recruitment Site for Ryans Recruit Firm
- Complete PostgreSQL schema with migrations - Node.js/Express backend with authentication - Public website (home, about, services, jobs, apply, contact) - Admin dashboard with applicant and job management - CV upload and storage in PostgreSQL BYTEA - Docker Compose setup for deployment - Session-based authentication - Responsive design with Ryan brand colors
This commit is contained in:
201
public/js/main.js
Normal file
201
public/js/main.js
Normal file
@@ -0,0 +1,201 @@
|
||||
// Main JavaScript for Ryans Recruit Firm
|
||||
|
||||
// Load jobs on jobs.html page
|
||||
if (window.location.pathname.includes('jobs.html')) {
|
||||
loadJobs();
|
||||
}
|
||||
|
||||
async function loadJobs() {
|
||||
const jobsContainer = document.getElementById('jobs-container');
|
||||
const loadingEl = document.getElementById('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/jobs');
|
||||
const jobs = await response.json();
|
||||
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
|
||||
if (jobs.length === 0) {
|
||||
jobsContainer.innerHTML = '<div class="text-center"><p>No job openings at the moment. Check back soon!</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
jobsContainer.innerHTML = jobs.map(job => `
|
||||
<div class="job-card">
|
||||
<div class="job-header">
|
||||
<div>
|
||||
<h3 class="job-title">${escapeHtml(job.title)}</h3>
|
||||
<div class="job-meta">
|
||||
<span class="job-meta-item">📍 ${escapeHtml(job.location || 'Not specified')}</span>
|
||||
<span class="job-meta-item">💼 ${escapeHtml(job.employment_type || 'Full-time')}</span>
|
||||
${job.salary_range ? `<span class="job-meta-item">💰 ${escapeHtml(job.salary_range)}</span>` : ''}
|
||||
${job.department ? `<span class="tag tag-primary">${escapeHtml(job.department)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="job-description">${escapeHtml(job.description.substring(0, 200))}...</p>
|
||||
<a href="/apply.html?job=${job.id}" class="btn btn-primary">Apply Now</a>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (err) {
|
||||
console.error('Error loading jobs:', err);
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
jobsContainer.innerHTML = '<div class="alert alert-error">Failed to load job listings. Please try again later.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle application form on apply.html
|
||||
if (window.location.pathname.includes('apply.html')) {
|
||||
loadJobDetails();
|
||||
document.getElementById('application-form')?.addEventListener('submit', handleApplicationSubmit);
|
||||
}
|
||||
|
||||
async function loadJobDetails() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const jobId = urlParams.get('job');
|
||||
|
||||
if (!jobId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/jobs/${jobId}`);
|
||||
const job = await response.json();
|
||||
|
||||
document.getElementById('job-title').textContent = job.title;
|
||||
document.getElementById('job-details').innerHTML = `
|
||||
<p><strong>Location:</strong> ${escapeHtml(job.location)}</p>
|
||||
<p><strong>Type:</strong> ${escapeHtml(job.employment_type)}</p>
|
||||
${job.salary_range ? `<p><strong>Salary:</strong> ${escapeHtml(job.salary_range)}</p>` : ''}
|
||||
<p><strong>Description:</strong></p>
|
||||
<p>${escapeHtml(job.description)}</p>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error('Error loading job details:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApplicationSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="loading"></span> Submitting...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/apply', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Application submitted successfully! We\'ll be in touch soon.', 'success');
|
||||
form.reset();
|
||||
setTimeout(() => window.location.href = '/jobs.html', 2000);
|
||||
} else {
|
||||
showAlert(result.error || 'Failed to submit application', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error submitting application:', err);
|
||||
showAlert('Failed to submit application. Please try again.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalBtnText;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle contact form on contact.html
|
||||
if (window.location.pathname.includes('contact.html')) {
|
||||
document.getElementById('contact-form')?.addEventListener('submit', handleContactSubmit);
|
||||
}
|
||||
|
||||
async function handleContactSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="loading"></span> Sending...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Message sent successfully! We\'ll get back to you soon.', 'success');
|
||||
form.reset();
|
||||
} else {
|
||||
showAlert(result.error || 'Failed to send message', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error sending message:', err);
|
||||
showAlert('Failed to send message. Please try again.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalBtnText;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showAlert(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type}`;
|
||||
alertDiv.textContent = message;
|
||||
alertDiv.style.position = 'fixed';
|
||||
alertDiv.style.top = '20px';
|
||||
alertDiv.style.right = '20px';
|
||||
alertDiv.style.zIndex = '10000';
|
||||
alertDiv.style.minWidth = '300px';
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// File input validation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInputs = document.querySelectorAll('input[type="file"]');
|
||||
fileInputs.forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||
if (file.size > maxSize) {
|
||||
showAlert('File size must be less than 5MB', 'error');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
showAlert('Only PDF, DOC, and DOCX files are allowed', 'error');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user