1190 lines
33 KiB
HTML
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 %} |