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:
290
public/js/admin.js
Normal file
290
public/js/admin.js
Normal file
@@ -0,0 +1,290 @@
|
||||
// Admin JavaScript for Ryans Recruit Firm
|
||||
|
||||
// Check authentication on all admin pages except login
|
||||
if (!window.location.pathname.includes('login.html')) {
|
||||
checkAuth();
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/check');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.loggedIn) {
|
||||
window.location.href = '/admin/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update admin name in header if exists
|
||||
const adminNameEl = document.getElementById('admin-name');
|
||||
if (adminNameEl && data.admin) {
|
||||
adminNameEl.textContent = data.admin.fullName;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Auth check failed:', err);
|
||||
window.location.href = '/admin/login.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Login page handlers
|
||||
if (window.location.pathname.includes('login.html')) {
|
||||
checkFirstAdmin();
|
||||
document.getElementById('login-form')?.addEventListener('submit', handleLogin);
|
||||
}
|
||||
|
||||
async function checkFirstAdmin() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/check-first');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.isFirstAdmin) {
|
||||
document.getElementById('first-admin-notice').style.display = 'block';
|
||||
document.getElementById('full-name-group').style.display = 'block';
|
||||
document.querySelector('button[type="submit"]').textContent = 'Create Admin Account';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking first admin:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="loading"></span> Processing...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Login successful!', 'success');
|
||||
setTimeout(() => window.location.href = '/admin/dashboard.html', 500);
|
||||
} else {
|
||||
showAlert(result.error || 'Login failed', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
showAlert('Login failed. Please try again.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard page
|
||||
if (window.location.pathname.includes('dashboard.html')) {
|
||||
loadDashboardStats();
|
||||
}
|
||||
|
||||
async function loadDashboardStats() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/stats');
|
||||
const stats = await response.json();
|
||||
|
||||
document.getElementById('stat-new-applications').textContent = stats.new_applications || 0;
|
||||
document.getElementById('stat-total-applications').textContent = stats.total_applications || 0;
|
||||
document.getElementById('stat-total-applicants').textContent = stats.total_applicants || 0;
|
||||
document.getElementById('stat-active-jobs').textContent = stats.active_jobs || 0;
|
||||
|
||||
if (stats.unread_messages > 0) {
|
||||
document.getElementById('stat-unread-messages').textContent = stats.unread_messages;
|
||||
document.getElementById('stat-unread-messages').parentElement.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading stats:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Applications page
|
||||
if (window.location.pathname.includes('applicants.html')) {
|
||||
loadApplications();
|
||||
}
|
||||
|
||||
async function loadApplications(filters = {}) {
|
||||
const container = document.getElementById('applications-container');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams(filters);
|
||||
const response = await fetch(`/api/admin/applications?${params}`);
|
||||
const applications = await response.json();
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
if (applications.length === 0) {
|
||||
container.innerHTML = '<div class="text-center"><p>No applications found.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Applicant</th>
|
||||
<th>Job</th>
|
||||
<th>Experience</th>
|
||||
<th>Applied</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${applications.map(app => `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${escapeHtml(app.full_name)}</strong><br>
|
||||
<small>${escapeHtml(app.email)}</small>
|
||||
</td>
|
||||
<td>${escapeHtml(app.job_title || 'General Application')}</td>
|
||||
<td>${app.years_of_experience || 'N/A'} years</td>
|
||||
<td>${new Date(app.applied_at).toLocaleDateString()}</td>
|
||||
<td><span class="tag tag-${getStatusColor(app.status)}">${escapeHtml(app.status)}</span></td>
|
||||
<td>
|
||||
<a href="/admin/applicants.html?id=${app.id}" class="btn btn-sm btn-primary">View</a>
|
||||
<a href="/api/admin/applications/${app.id}/cv" class="btn btn-sm btn-secondary" target="_blank">CV</a>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error('Error loading applications:', err);
|
||||
if (loading) loading.style.display = 'none';
|
||||
container.innerHTML = '<div class="alert alert-error">Failed to load applications</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Jobs management page
|
||||
if (window.location.pathname.includes('jobs.html') && window.location.pathname.includes('admin')) {
|
||||
loadAdminJobs();
|
||||
}
|
||||
|
||||
async function loadAdminJobs() {
|
||||
const container = document.getElementById('jobs-container');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/jobs');
|
||||
const jobs = await response.json();
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<button onclick="showJobForm()" class="btn btn-success">+ Create New Job</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Department</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${jobs.map(job => `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(job.title)}</strong></td>
|
||||
<td>${escapeHtml(job.department || 'N/A')}</td>
|
||||
<td>${escapeHtml(job.location || 'N/A')}</td>
|
||||
<td><span class="tag ${job.is_active ? 'tag-success' : 'tag-error'}">${job.is_active ? 'Active' : 'Inactive'}</span></td>
|
||||
<td>${new Date(job.created_at).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<button onclick="editJob(${job.id})" class="btn btn-sm btn-primary">Edit</button>
|
||||
<button onclick="toggleJobStatus(${job.id}, ${!job.is_active})" class="btn btn-sm btn-secondary">${job.is_active ? 'Deactivate' : 'Activate'}</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error('Error loading jobs:', err);
|
||||
if (loading) loading.style.display = 'none';
|
||||
container.innerHTML = '<div class="alert alert-error">Failed to load jobs</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleJobStatus(jobId, isActive) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/jobs/${jobId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isActive })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert(`Job ${isActive ? 'activated' : 'deactivated'} successfully`, 'success');
|
||||
loadAdminJobs();
|
||||
} else {
|
||||
showAlert('Failed to update job status', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating job:', err);
|
||||
showAlert('Failed to update job status', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Logout
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/api/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login.html';
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
window.location.href = '/admin/login.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
'new': 'primary',
|
||||
'reviewing': 'warning',
|
||||
'interview': 'info',
|
||||
'hired': 'success',
|
||||
'rejected': 'error'
|
||||
};
|
||||
return colors[status] || 'primary';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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