ATS/templates/applicant/application_submit_form.html
2026-02-01 13:38:06 +03:00

1190 lines
33 KiB
HTML

{% extends 'applicant/partials/candidate_facing_base.html' %}
{% load static i18n %}
{% block title %}{% trans "Career Application Form" %}{% endblock %}
{% block content %}
<div class="app-wrapper">
<!-- Header -->
<div class="app-header">
<div class="container">
<div class="header-content">
<div>
<h1 class="header-title">{{ job.title }}</h1>
<p class="header-subtitle">{{ job.department }}</p>
</div>
<div class="progress-badge" id="progressText">1 of 1</div>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressBar"></div>
</div>
</div>
</div>
<!-- Content -->
<div class="container">
<div class="form-card">
<!-- Stage Container -->
<div id="stageContainer"></div>
<!-- Preview Container -->
<div id="previewContainer" class="hidden">
<h2 class="preview-title">{% trans "Review Your Application" %}</h2>
<p class="preview-subtitle">{% trans "Please review your information before submitting" %}</p>
<div id="previewContent"></div>
</div>
<!-- Navigation -->
<div class="form-nav">
<button id="backBtn" class="btn-secondary hidden">
{% trans "Back" %}
</button>
<button id="nextBtn" class="btn-primary">
{% trans "Continue" %}
</button>
<button id="submitBtn" class="btn-primary hidden">
{% trans "Submit Application" %}
</button>
</div>
</div>
</div>
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--red: #9d2235;
--red-dark: #7a1a29;
--red-light: #fef2f2;
--gray-50: #fafafa;
--gray-100: #f5f5f5;
--gray-200: #e5e5e5;
--gray-300: #d4d4d4;
--gray-400: #a3a3a3;
--gray-500: #737373;
--gray-600: #525252;
--gray-700: #404040;
--gray-900: #171717;
--error: #dc2626;
--success: #16a34a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
color: var(--gray-900);
background: linear-gradient(to bottom, #ffffff 0%, var(--gray-50) 100%);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
.app-wrapper {
min-height: 100vh;
padding-bottom: 3rem;
}
.container {
max-width: 56rem;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Header */
.app-header {
background: white;
border-bottom: 1px solid var(--gray-200);
padding: 2rem 0 1.5rem;
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1.5rem;
margin-bottom: 1.25rem;
}
.header-title {
font-size: 1.75rem;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.02em;
color: var(--gray-900);
}
.header-subtitle {
font-size: 0.9375rem;
color: var(--gray-500);
margin-top: 0.375rem;
font-weight: 500;
}
.progress-badge {
background: linear-gradient(135deg, var(--red-light) 0%, #fff 100%);
padding: 0.5rem 1rem;
border-radius: 9999px;
font-size: 0.8125rem;
font-weight: 600;
color: var(--red);
white-space: nowrap;
border: 1px solid rgba(157, 34, 53, 0.1);
}
.progress-bar {
height: 0.375rem;
background: var(--gray-100);
border-radius: 9999px;
overflow: hidden;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--red) 0%, var(--red-dark) 100%);
width: 0;
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 0 10px rgba(157, 34, 53, 0.3);
}
/* Form Card */
.form-card {
background: white;
border: 1px solid var(--gray-200);
border-radius: 1.25rem;
padding: 3rem;
margin-top: 2.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
/* Stage */
.stage-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.75rem;
letter-spacing: -0.02em;
color: var(--gray-900);
}
.stage-description {
color: var(--gray-500);
margin-bottom: 2.5rem;
font-size: 1.0625rem;
line-height: 1.7;
}
/* Field */
.field {
margin-bottom: 2rem;
}
.field-label {
display: block;
font-size: 0.9375rem;
font-weight: 600;
color: var(--gray-700);
margin-bottom: 0.625rem;
}
.required {
color: var(--error);
margin-left: 0.125rem;
}
.field-input,
.field-select,
.field-textarea {
width: 100%;
padding: 0.875rem 1rem;
border: 1.5px solid var(--gray-300);
border-radius: 0.75rem;
font-size: 1rem;
font-family: inherit;
background: white;
transition: all 0.2s ease;
color: var(--gray-900);
}
.field-input:hover,
.field-select:hover,
.field-textarea:hover {
border-color: var(--gray-400);
}
.field-input:focus,
.field-select:focus,
.field-textarea:focus {
outline: none;
border-color: var(--red);
box-shadow: 0 0 0 3px rgba(157, 34, 53, 0.1);
}
.field-input.error,
.field-select.error,
.field-textarea.error {
border-color: var(--error);
background: var(--red-light);
}
.field-textarea {
min-height: 7rem;
resize: vertical;
line-height: 1.6;
}
.field-input::placeholder,
.field-textarea::placeholder {
color: var(--gray-400);
}
.field-error {
display: none;
font-size: 0.8125rem;
color: var(--error);
margin-top: 0.5rem;
font-weight: 500;
}
.field-error.show {
display: flex;
align-items: center;
gap: 0.375rem;
}
.field-error::before {
content: "⚠";
font-size: 0.875rem;
}
/* Options */
.option-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.option {
display: flex;
align-items: center;
padding: 1rem 1.25rem;
border: 1.5px solid var(--gray-300);
border-radius: 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
background: white;
}
.option:hover {
border-color: var(--red);
background: var(--red-light);
transform: translateX(2px);
}
.option input {
margin: 0;
margin-right: 1rem;
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
accent-color: var(--red);
}
.option label {
flex: 1;
cursor: pointer;
font-size: 0.9375rem;
font-weight: 500;
color: var(--gray-700);
}
.option:has(input:checked) {
border-color: var(--red);
background: var(--red-light);
box-shadow: 0 0 0 3px rgba(157, 34, 53, 0.1);
}
/* File Upload */
.file-upload {
border: 2px dashed var(--gray-300);
border-radius: 1rem;
padding: 2.5rem;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
background: var(--gray-50);
}
.file-upload:hover {
border-color: var(--red);
background: var(--red-light);
}
.file-upload.error {
border-color: var(--error);
background: var(--red-light);
}
.file-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.file-text {
font-size: 1rem;
color: var(--gray-700);
margin-bottom: 0.625rem;
font-weight: 500;
}
.file-text strong {
color: var(--red);
}
.file-hint {
font-size: 0.8125rem;
color: var(--gray-500);
}
.file-input {
display: none;
}
.uploaded-file {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
background: white;
border: 1.5px solid var(--gray-200);
border-radius: 0.75rem;
margin-top: 1rem;
}
.file-info {
display: flex;
align-items: center;
gap: 1rem;
}
.file-info::before {
content: "📄";
font-size: 1.5rem;
}
.file-name {
font-size: 0.9375rem;
font-weight: 600;
color: var(--gray-900);
}
.file-size {
font-size: 0.75rem;
color: var(--gray-500);
margin-top: 0.125rem;
}
.remove-btn {
background: var(--gray-100);
border: none;
color: var(--gray-500);
cursor: pointer;
padding: 0.5rem;
font-size: 1.125rem;
border-radius: 0.5rem;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.remove-btn:hover {
background: var(--error);
color: white;
}
/* Preview */
.preview-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.75rem;
letter-spacing: -0.02em;
}
.preview-subtitle {
color: var(--gray-500);
margin-bottom: 2.5rem;
font-size: 1.0625rem;
}
.preview-section {
padding: 2rem;
background: var(--gray-50);
border-radius: 1rem;
margin-bottom: 1.5rem;
border: 1px solid var(--gray-200);
}
.preview-section-title {
font-size: 0.875rem;
font-weight: 700;
color: var(--red);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1.5rem;
}
.preview-field {
margin-bottom: 1.25rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--gray-200);
}
.preview-field:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.preview-label {
font-size: 0.8125rem;
font-weight: 600;
color: var(--gray-500);
margin-bottom: 0.375rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.preview-value {
font-size: 1rem;
color: var(--gray-900);
font-weight: 500;
}
/* Navigation */
.form-nav {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--gray-200);
}
.btn-primary,
.btn-secondary {
padding: 1rem 2rem;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.btn-primary {
background: linear-gradient(135deg, var(--red) 0%, var(--red-dark) 100%);
color: white;
margin-left: auto;
box-shadow: 0 2px 8px rgba(157, 34, 53, 0.2);
}
.btn-primary:hover:not(:disabled) {
box-shadow: 0 4px 12px rgba(157, 34, 53, 0.3);
transform: translateY(-1px);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: white;
color: var(--gray-700);
border: 1.5px solid var(--gray-300);
}
.btn-secondary:hover {
background: var(--gray-50);
border-color: var(--gray-400);
}
.hidden {
display: none !important;
}
/* Loading State */
@keyframes spin {
to { transform: rotate(360deg); }
}
.btn-primary:disabled::after {
content: "";
display: inline-block;
width: 0.875rem;
height: 0.875rem;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-left: 0.5rem;
}
/* Mobile */
@media (max-width: 640px) {
.app-header {
padding: 1.5rem 0 1rem;
}
.header-title {
font-size: 1.375rem;
}
.form-card {
padding: 2rem 1.5rem;
}
.stage-title {
font-size: 1.625rem;
}
.preview-title {
font-size: 1.625rem;
}
.btn-primary,
.btn-secondary {
padding: 0.875rem 1.5rem;
font-size: 0.9375rem;
}
.form-nav {
margin-top: 2rem;
}
.option {
padding: 0.875rem 1rem;
}
}
/* Smooth transitions */
* {
transition-property: background-color, border-color, color, box-shadow, transform;
transition-duration: 0.2s;
transition-timing-function: ease;
}
input, select, textarea, button {
transition-property: all;
}
</style>
<script>
// State
const state = {
jobSlug: '{{ job_slug }}',
stages: [],
currentStage: 0,
formData: {},
isPreview: false,
errors: {}
};
// DOM Elements
const dom = {
progressBar: document.getElementById('progressBar'),
progressText: document.getElementById('progressText'),
stageContainer: document.getElementById('stageContainer'),
previewContainer: document.getElementById('previewContainer'),
previewContent: document.getElementById('previewContent'),
backBtn: document.getElementById('backBtn'),
nextBtn: document.getElementById('nextBtn'),
submitBtn: document.getElementById('submitBtn')
};
// Validation
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function validatePhone(phone) {
const digits = phone.replace(/\D/g, '');
return digits.length >= 10 && digits.length <= 15;
}
function validateField(field, value) {
delete state.errors[field.id];
// Required check
if (field.required) {
if (!value || (typeof value === 'string' && !value.trim()) ||
(Array.isArray(value) && value.length === 0)) {
state.errors[field.id] = 'This field is required';
return false;
}
}
if (!value) return true;
// Type-specific validation
switch (field.type) {
case 'email':
if (!validateEmail(value)) {
state.errors[field.id] = 'Please enter a valid email';
return false;
}
break;
case 'phone':
if (!validatePhone(value)) {
state.errors[field.id] = 'Please enter a valid phone number';
return false;
}
break;
case 'file':
if (value instanceof File) {
const maxSize = (field.maxFileSize || 5) * 1024 * 1024;
if (value.size > maxSize) {
state.errors[field.id] = `File too large (max ${field.maxFileSize || 5}MB)`;
return false;
}
}
break;
}
return true;
}
function validateStage() {
const stage = state.stages[state.currentStage];
let isValid = true;
stage.fields.forEach(field => {
if (!validateField(field, state.formData[field.id])) {
isValid = false;
}
});
showErrors();
return isValid;
}
function showErrors() {
// Hide all errors
document.querySelectorAll('.field-error').forEach(el => {
el.classList.remove('show');
});
// Remove error styling
document.querySelectorAll('.error').forEach(el => {
el.classList.remove('error');
});
// Show current errors
Object.keys(state.errors).forEach(fieldId => {
const errorEl = document.getElementById(`error_${fieldId}`);
const inputEl = document.getElementById(`field_${fieldId}`);
if (errorEl) {
errorEl.textContent = state.errors[fieldId];
errorEl.classList.add('show');
}
if (inputEl) {
inputEl.classList.add('error');
}
});
}
// Rendering
function updateProgress() {
const total = state.stages.length;
const current = state.isPreview ? total : state.currentStage + 1;
const percent = (current / total) * 100;
dom.progressBar.style.width = `${percent}%`;
dom.progressText.textContent = state.isPreview ? 'Review' : `${current} of ${total}`;
}
function renderStage() {
dom.stageContainer.classList.remove('hidden');
dom.previewContainer.classList.add('hidden');
dom.stageContainer.innerHTML = '';
const stage = state.stages[state.currentStage];
// Stage header
const header = document.createElement('div');
header.innerHTML = `
<h2 class="stage-title">${stage.name}</h2>
${stage.description ? `<p class="stage-description">${stage.description}</p>` : ''}
`;
dom.stageContainer.appendChild(header);
// Fields
stage.fields.forEach(field => {
dom.stageContainer.appendChild(createField(field));
});
updateButtons();
}
function createField(field) {
const fieldDiv = document.createElement('div');
fieldDiv.className = 'field';
const label = document.createElement('label');
label.className = 'field-label';
label.htmlFor = `field_${field.id}`;
label.innerHTML = `${field.label}${field.required ? ' <span class="required">*</span>' : ''}`;
fieldDiv.appendChild(label);
// Create input based on type
if (field.type === 'text' || field.type === 'email' || field.type === 'phone') {
const input = document.createElement('input');
input.className = 'field-input';
input.id = `field_${field.id}`;
input.type = field.type === 'email' ? 'email' : field.type === 'phone' ? 'tel' : 'text';
input.value = state.formData[field.id] || '';
input.placeholder = field.placeholder || '';
input.addEventListener('input', (e) => {
state.formData[field.id] = e.target.value;
delete state.errors[field.id];
showErrors();
});
fieldDiv.appendChild(input);
}
else if (field.type === 'textarea') {
const textarea = document.createElement('textarea');
textarea.className = 'field-textarea';
textarea.id = `field_${field.id}`;
textarea.value = state.formData[field.id] || '';
textarea.placeholder = field.placeholder || '';
textarea.addEventListener('input', (e) => {
state.formData[field.id] = e.target.value;
delete state.errors[field.id];
showErrors();
});
fieldDiv.appendChild(textarea);
}
else if (field.type === 'date') {
const input = document.createElement('input');
input.className = 'field-input';
input.id = `field_${field.id}`;
input.type = 'date';
input.value = state.formData[field.id] || '';
input.addEventListener('input', (e) => {
state.formData[field.id] = e.target.value;
delete state.errors[field.id];
showErrors();
});
fieldDiv.appendChild(input);
}
else if (field.type === 'select') {
const select = document.createElement('select');
select.className = 'field-select';
select.id = `field_${field.id}`;
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = `Select ${field.label}`;
select.appendChild(defaultOption);
field.options.forEach(opt => {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
option.selected = state.formData[field.id] === opt;
select.appendChild(option);
});
select.addEventListener('change', (e) => {
state.formData[field.id] = e.target.value;
delete state.errors[field.id];
showErrors();
});
fieldDiv.appendChild(select);
}
else if (field.type === 'radio') {
const group = document.createElement('div');
group.className = 'option-group';
field.options.forEach((opt, i) => {
const option = document.createElement('div');
option.className = 'option';
const input = document.createElement('input');
input.type = 'radio';
input.id = `field_${field.id}_${i}`;
input.name = `field_${field.id}`;
input.value = opt;
input.checked = state.formData[field.id] === opt;
input.addEventListener('change', () => {
state.formData[field.id] = opt;
delete state.errors[field.id];
showErrors();
});
const lbl = document.createElement('label');
lbl.htmlFor = input.id;
lbl.textContent = opt;
option.appendChild(input);
option.appendChild(lbl);
group.appendChild(option);
});
fieldDiv.appendChild(group);
}
else if (field.type === 'checkbox') {
const group = document.createElement('div');
group.className = 'option-group';
field.options.forEach((opt, i) => {
const option = document.createElement('div');
option.className = 'option';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `field_${field.id}_${i}`;
input.value = opt;
input.checked = Array.isArray(state.formData[field.id]) &&
state.formData[field.id].includes(opt);
input.addEventListener('change', () => {
if (!state.formData[field.id]) state.formData[field.id] = [];
if (input.checked) {
state.formData[field.id].push(opt);
} else {
state.formData[field.id] = state.formData[field.id].filter(v => v !== opt);
}
delete state.errors[field.id];
showErrors();
});
const lbl = document.createElement('label');
lbl.htmlFor = input.id;
lbl.textContent = opt;
option.appendChild(input);
option.appendChild(lbl);
group.appendChild(option);
});
fieldDiv.appendChild(group);
}
else if (field.type === 'file') {
const upload = document.createElement('div');
upload.className = 'file-upload';
upload.id = `field_${field.id}`;
upload.innerHTML = `
<div class="file-icon">📄</div>
<div class="file-text">Click to upload ${field.label}</div>
<div class="file-hint">Supports: ${field.fileTypes || '.pdf, .doc, .docx'} (Max ${field.maxFileSize || 5}MB)</div>
<input type="file" class="file-input" accept="${field.fileTypes || '.pdf,.doc,.docx'}">
`;
const fileInput = upload.querySelector('.file-input');
upload.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
state.formData[field.id] = file;
delete state.errors[field.id];
showErrors();
const fileInfo = document.createElement('div');
fileInfo.className = 'uploaded-file';
fileInfo.innerHTML = `
<div class="file-info">
<div>
<div class="file-name">${file.name}</div>
<div class="file-size">${(file.size / 1024).toFixed(1)} KB</div>
</div>
</div>
<button type="button" class="remove-btn">✕</button>
`;
fileInfo.querySelector('.remove-btn').addEventListener('click', (e) => {
e.stopPropagation();
delete state.formData[field.id];
fileInfo.remove();
fileInput.value = '';
});
fieldDiv.appendChild(fileInfo);
}
});
fieldDiv.appendChild(upload);
}
// Error message
const error = document.createElement('div');
error.className = 'field-error';
error.id = `error_${field.id}`;
fieldDiv.appendChild(error);
return fieldDiv;
}
function renderPreview() {
dom.stageContainer.classList.add('hidden');
dom.previewContainer.classList.remove('hidden');
dom.previewContent.innerHTML = '';
state.stages.forEach(stage => {
const section = document.createElement('div');
section.className = 'preview-section';
const title = document.createElement('div');
title.className = 'preview-section-title';
title.textContent = stage.name;
section.appendChild(title);
stage.fields.forEach(field => {
const fieldDiv = document.createElement('div');
fieldDiv.className = 'preview-field';
const label = document.createElement('div');
label.className = 'preview-label';
label.textContent = field.label;
fieldDiv.appendChild(label);
const value = document.createElement('div');
value.className = 'preview-value';
let displayValue = state.formData[field.id];
if (!displayValue) {
displayValue = '—';
} else if (field.type === 'file' && displayValue instanceof File) {
displayValue = displayValue.name;
} else if (field.type === 'checkbox' && Array.isArray(displayValue)) {
displayValue = displayValue.join(', ');
}
value.textContent = displayValue;
fieldDiv.appendChild(value);
section.appendChild(fieldDiv);
});
dom.previewContent.appendChild(section);
});
updateButtons();
}
function updateButtons() {
const isLast = state.currentStage === state.stages.length - 1;
dom.backBtn.classList.toggle('hidden', state.currentStage === 0 && !state.isPreview);
dom.nextBtn.classList.toggle('hidden', state.isPreview);
dom.submitBtn.classList.toggle('hidden', !state.isPreview);
if (!state.isPreview) {
dom.nextBtn.textContent = isLast ? 'Review' : 'Continue';
}
}
// Navigation
function next() {
if (state.isPreview) {
submit();
return;
}
if (!validateStage()) {
const firstError = document.querySelector('.field-error.show');
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return;
}
if (state.currentStage === state.stages.length - 1) {
state.isPreview = true;
renderPreview();
updateProgress();
} else {
state.currentStage++;
renderStage();
updateProgress();
window.scrollTo(0, 0);
}
}
function back() {
if (state.isPreview) {
state.isPreview = false;
state.currentStage = state.stages.length - 1;
renderStage();
updateProgress();
} else if (state.currentStage > 0) {
state.currentStage--;
renderStage();
updateProgress();
window.scrollTo(0, 0);
}
}
async function submit() {
// Disable submit button to prevent double submission
dom.submitBtn.disabled = true;
dom.submitBtn.textContent = 'Submitting...';
const formData = new FormData();
// Note: Backend has @csrf_exempt but including token doesn't hurt
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
state.stages.forEach(stage => {
stage.fields.forEach(field => {
const value = state.formData[field.id];
if (field.type === 'file') {
if (value instanceof File) {
formData.append(`field_${field.id}`, value);
}
// Don't append empty string for files - backend checks for file existence
} else if (field.type === 'checkbox') {
const checkboxValue = Array.isArray(value) ? value.join(', ') : '';
formData.append(`field_${field.id}`, checkboxValue);
} else {
formData.append(`field_${field.id}`, value || '');
}
});
});
try {
const response = await fetch(`/application/${state.jobSlug}/submit/`, {
method: 'POST',
body: formData,
credentials: 'same-origin' // Include cookies for authentication
});
// Check if we got redirected to login (status 302/200 with HTML)
if (response.redirected) {
window.location.href = response.url;
return;
}
// Get response text first
const responseText = await response.text();
// Log for debugging
console.log('Response status:', response.status);
console.log('Response headers:', response.headers.get('content-type'));
// Check if response looks like JSON
let result;
try {
result = JSON.parse(responseText);
} catch (e) {
// Not JSON - probably an error page or redirect
console.error('Server response (non-JSON):', responseText.substring(0, 500));
// Check if it looks like HTML
if (responseText.trim().startsWith('<!DOCTYPE') || responseText.trim().startsWith('<html')) {
alert('Session expired. Please log in again.');
window.location.reload();
return;
}
throw new Error('Server error - invalid response format');
}
if (result.success) {
// Success! Redirect to success page
if (result.redirect_url) {
window.location.href = result.redirect_url;
} else {
alert('Application submitted successfully!');
window.location.href = `/jobs/${state.jobSlug}/`;
}
} else {
// Server returned an error
alert('Error: ' + (result.error || result.message || 'Submission failed'));
dom.submitBtn.disabled = false;
dom.submitBtn.textContent = 'Submit Application';
}
} catch (error) {
console.error('Submission error:', error);
alert(error.message || 'Error submitting application. Please try again.');
dom.submitBtn.disabled = false;
dom.submitBtn.textContent = 'Submit Application';
}
}
// Load template
async function loadTemplate() {
try {
const response = await fetch(`/api/v1/templates/${state.jobSlug}/`);
console.log('Template API response status:', response.status);
// Check if redirected
if (response.redirected) {
window.location.href = response.url;
return;
}
// Get response text first
const responseText = await response.text();
// Try to parse as JSON
let result;
try {
result = JSON.parse(responseText);
} catch (e) {
console.error('Template API response (non-JSON):', responseText.substring(0, 500));
// Check if HTML (login page)
if (responseText.trim().startsWith('<!DOCTYPE') || responseText.trim().startsWith('<html')) {
alert('Session expired. Please log in again.');
window.location.reload();
return;
}
throw new Error('Server error loading form template');
}
if (result.success && result.template && result.template.stages) {
state.stages = result.template.stages;
console.log('Loaded stages:', state.stages.length);
updateProgress();
renderStage();
} else {
console.error('Invalid template structure:', result);
throw new Error(result.error || 'Invalid form template');
}
} catch (error) {
console.error('Error loading template:', error);
alert(error.message || 'Error loading form. Please refresh the page.');
}
}
// Initialize
dom.nextBtn.addEventListener('click', next);
dom.backBtn.addEventListener('click', back);
dom.submitBtn.addEventListener('click', submit);
document.addEventListener('DOMContentLoaded', loadTemplate);
</script>
{% endblock content %}