967 lines
34 KiB
HTML
967 lines
34 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ form.title }} - Form Preview{% endblock %}
|
|
|
|
{% block content %}
|
|
{% if not is_embed %}
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-6">
|
|
<div class="flex-1">
|
|
<h1 class="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
|
<div class="bg-temple-red/10 p-3 rounded-xl">
|
|
<i data-lucide="layout-template" class="w-8 h-8 text-temple-red"></i>
|
|
</div>
|
|
Form Preview
|
|
</h1>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<a href="{% url 'form_list' %}" class="inline-flex items-center gap-2 border border-gray-300 text-gray-600 hover:bg-gray-100 px-4 py-2.5 rounded-xl text-sm transition">
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i> Back to Forms
|
|
</a>
|
|
<a href="{% url 'form_embed' form.id %}" class="inline-flex items-center gap-2 border border-temple-red text-temple-red hover:bg-temple-red hover:text-white px-4 py-2.5 rounded-xl text-sm transition font-medium" target="_blank">
|
|
<i data-lucide="code-2" class="w-4 h-4"></i> Get Embed Code
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Form Preview Container -->
|
|
<div class="{% if is_embed %}bg-gray-100 min-h-screen{% else %}max-w-7xl mx-auto px-4 sm:px-6 lg:px-8{% endif %}">
|
|
<div class="{% if is_embed %}{% else %}flex justify-center{% endif %}">
|
|
<div class="{% if is_embed %}w-full{% else %}w-full max-w-4xl{% endif %}">
|
|
<!-- Form Header -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mb-4 overflow-hidden">
|
|
<div class="bg-gradient-to-br from-temple-red to-[#7a1a29] text-white p-6">
|
|
<h3 class="text-2xl font-bold mb-2">{{ form.title }}</h3>
|
|
{% if form.description %}
|
|
<p class="text-white/90 mb-0">{{ form.description }}</p>
|
|
{% endif %}
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3">
|
|
<span class="text-sm text-gray-600 flex items-center gap-2">
|
|
<i data-lucide="bar-chart-3" class="w-4 h-4"></i> {{ submission_count }} submissions
|
|
</span>
|
|
<span class="text-sm text-gray-600 flex items-center gap-2">
|
|
<i data-lucide="calendar" class="w-4 h-4"></i> Created {{ form.created_at|date:"M d, Y" }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Form Preview -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
|
<div class="p-0">
|
|
<div id="form-preview-container">
|
|
<!-- Form will be rendered here by Preact -->
|
|
<div class="text-center py-12">
|
|
<div class="inline-block w-12 h-12 border-4 border-temple-red border-t-transparent rounded-full animate-spin"></div>
|
|
<p class="mt-4 text-gray-500">Loading form preview...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Analytics (only shown if not embedded) -->
|
|
{% if not is_embed %}
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 mt-6 overflow-hidden">
|
|
<div class="border-b border-gray-100 p-6">
|
|
<h5 class="text-lg font-bold text-gray-900 flex items-center gap-2">
|
|
<i data-lucide="line-chart" class="w-5 h-5 text-temple-red"></i> Form Analytics
|
|
</h5>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
<div class="text-center p-4 bg-gray-50 rounded-xl">
|
|
<h4 class="text-2xl font-bold text-temple-red">{{ submission_count }}</h4>
|
|
<span class="text-sm text-gray-600 mt-1 block">Total Submissions</span>
|
|
</div>
|
|
<div class="text-center p-4 bg-gray-50 rounded-xl">
|
|
<h4 class="text-2xl font-bold text-emerald-600">{{ form.created_at|timesince }}</h4>
|
|
<span class="text-sm text-gray-600 mt-1 block">Time Created</span>
|
|
</div>
|
|
<div class="text-center p-4 bg-gray-50 rounded-xl">
|
|
<h4 class="text-2xl font-bold text-blue-600">{{ form.structure.wizards|length|default:0 }}</h4>
|
|
<span class="text-sm text-gray-600 mt-1 block">Form Steps</span>
|
|
</div>
|
|
<div class="text-center p-4 bg-gray-50 rounded-xl">
|
|
<h4 class="text-2xl font-bold text-amber-600">
|
|
{% if form.structure.wizards %}
|
|
{{ form.structure.wizards.0.fields|length|default:0 }}
|
|
{% else %}
|
|
0
|
|
{% endif %}
|
|
</h4>
|
|
<span class="text-sm text-gray-600 mt-1 block">First Step Fields</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success Modal -->
|
|
<div id="successModal" class="fixed inset-0 z-50 hidden">
|
|
<div class="absolute inset-0 bg-black/50 transition-opacity" onclick="closeModal('successModal')"></div>
|
|
<div class="relative min-h-screen flex items-center justify-center p-4">
|
|
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h5 class="text-xl font-bold text-gray-900 flex items-center gap-2">
|
|
<i data-lucide="check-circle" class="w-6 h-6 text-emerald-500"></i> Form Submitted Successfully!
|
|
</h5>
|
|
<button type="button" onclick="closeModal('successModal')" class="text-gray-400 hover:text-gray-600 transition">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mb-6">
|
|
<p class="text-gray-600 mb-0">Thank you for submitting this form. Your response has been recorded.</p>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('successModal')" class="flex-1 border border-gray-300 text-gray-600 hover:bg-gray-100 px-4 py-2.5 rounded-xl text-sm transition">Close</button>
|
|
<button type="button" onclick="resetForm(); closeModal('successModal');" class="flex-1 bg-temple-red hover:bg-[#7a1a29] text-white px-4 py-2.5 rounded-xl text-sm transition font-medium">Submit Another Response</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Modal -->
|
|
<div id="errorModal" class="fixed inset-0 z-50 hidden">
|
|
<div class="absolute inset-0 bg-black/50 transition-opacity" onclick="closeModal('errorModal')"></div>
|
|
<div class="relative min-h-screen flex items-center justify-center p-4">
|
|
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h5 class="text-xl font-bold text-gray-900 flex items-center gap-2">
|
|
<i data-lucide="alert-triangle" class="w-6 h-6 text-red-500"></i> Submission Error
|
|
</h5>
|
|
<button type="button" onclick="closeModal('errorModal')" class="text-gray-400 hover:text-gray-600 transition">
|
|
<i data-lucide="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mb-6">
|
|
<p class="text-gray-600 mb-0" id="errorMessage">An error occurred while submitting the form. Please try again.</p>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeModal('errorModal')" class="flex-1 border border-gray-300 text-gray-600 hover:bg-gray-100 px-4 py-2.5 rounded-xl text-sm transition">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
{% if is_embed %}
|
|
<style>
|
|
body {
|
|
background: #f3f4f6;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
</style>
|
|
{% endif %}
|
|
<style>
|
|
/* Form Styles */
|
|
.form-label {
|
|
@apply block text-sm font-semibold text-gray-700 mb-2;
|
|
}
|
|
|
|
.form-control {
|
|
@apply w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition;
|
|
}
|
|
|
|
.form-control.is-invalid {
|
|
@apply border-red-500 focus:border-red-500 focus:ring-red-500/20;
|
|
}
|
|
|
|
.form-select {
|
|
@apply w-full px-4 py-2.5 border border-gray-300 rounded-xl text-sm text-gray-900 bg-white focus:outline-none focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red transition;
|
|
}
|
|
|
|
.form-select.is-invalid {
|
|
@apply border-red-500 focus:border-red-500 focus:ring-red-500/20;
|
|
}
|
|
|
|
.form-check {
|
|
@apply flex items-start gap-3 mb-3;
|
|
}
|
|
|
|
.form-check-input {
|
|
@apply mt-1 w-4 h-4 text-temple-red border-gray-300 rounded focus:ring-2 focus:ring-temple-red/20;
|
|
}
|
|
|
|
.form-check-label {
|
|
@apply text-sm text-gray-700 cursor-pointer;
|
|
}
|
|
|
|
.invalid-feedback {
|
|
@apply text-red-500 text-sm mt-1;
|
|
}
|
|
|
|
.text-danger {
|
|
@apply text-red-500;
|
|
}
|
|
|
|
.text-muted {
|
|
@apply text-gray-500;
|
|
}
|
|
|
|
.form-text {
|
|
@apply text-sm text-gray-500;
|
|
}
|
|
|
|
/* Progress Bar */
|
|
.progress {
|
|
@apply w-full bg-gray-200 rounded-full overflow-hidden;
|
|
}
|
|
|
|
.progress-bar {
|
|
@apply bg-temple-red h-full transition-all duration-300;
|
|
}
|
|
|
|
/* Rating Stars */
|
|
.rating-star {
|
|
@apply cursor-pointer transition-transform hover:scale-110;
|
|
}
|
|
|
|
.rating-star.active {
|
|
@apply text-amber-400;
|
|
}
|
|
|
|
/* FilePond Custom Styles */
|
|
.filepond--root {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.filepond--drop-label {
|
|
@apply text-gray-600;
|
|
}
|
|
|
|
.filepond--label-action {
|
|
@apply text-temple-red font-semibold hover:text-temple-dark;
|
|
}
|
|
|
|
/* Button Styles */
|
|
.btn {
|
|
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium transition cursor-pointer border-0 outline-none;
|
|
}
|
|
|
|
.btn-primary {
|
|
@apply bg-temple-red hover:bg-[#7a1a29] text-white;
|
|
}
|
|
|
|
.btn-primary:disabled {
|
|
@apply opacity-50 cursor-not-allowed;
|
|
}
|
|
|
|
.btn-outline-secondary {
|
|
@apply border border-gray-300 text-gray-600 hover:bg-gray-100;
|
|
}
|
|
|
|
.btn-outline-secondary:disabled {
|
|
@apply opacity-50 cursor-not-allowed;
|
|
}
|
|
|
|
.btn-secondary {
|
|
@apply border border-gray-300 text-gray-600 hover:bg-gray-100;
|
|
}
|
|
|
|
/* Spinner */
|
|
.spinner-border {
|
|
@apply inline-block w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin;
|
|
}
|
|
|
|
.spinner-border-sm {
|
|
@apply w-3 h-3 border-2;
|
|
}
|
|
|
|
.spinner-border.text-primary {
|
|
@apply border-temple-red text-temple-red;
|
|
}
|
|
|
|
/* Utility */
|
|
.visually-hidden {
|
|
@apply absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0;
|
|
}
|
|
|
|
.d-flex {
|
|
@apply flex;
|
|
}
|
|
|
|
.justify-content-between {
|
|
@apply justify-between;
|
|
}
|
|
|
|
.align-items-center {
|
|
@apply items-center;
|
|
}
|
|
|
|
.mb-3 {
|
|
@apply mb-3;
|
|
}
|
|
|
|
.mb-4 {
|
|
@apply mb-4;
|
|
}
|
|
|
|
.me-2 {
|
|
@apply me-2;
|
|
}
|
|
|
|
.mt-1 {
|
|
@apply mt-1;
|
|
}
|
|
|
|
.mt-4 {
|
|
@apply mt-4;
|
|
}
|
|
|
|
.py-5 {
|
|
@apply py-12;
|
|
}
|
|
|
|
.text-center {
|
|
@apply text-center;
|
|
}
|
|
|
|
.text-primary {
|
|
@apply text-temple-red;
|
|
}
|
|
|
|
.text-success {
|
|
@apply text-emerald-600;
|
|
}
|
|
|
|
.text-info {
|
|
@apply text-blue-600;
|
|
}
|
|
|
|
.text-warning {
|
|
@apply text-amber-600;
|
|
}
|
|
|
|
.fa-4x {
|
|
@apply text-5xl;
|
|
}
|
|
|
|
.small {
|
|
@apply text-sm;
|
|
}
|
|
|
|
.modal {
|
|
@apply fixed inset-0 z-50;
|
|
}
|
|
|
|
.modal-dialog-centered {
|
|
@apply flex items-center justify-center min-h-screen p-4;
|
|
}
|
|
|
|
.modal-content {
|
|
@apply bg-white rounded-2xl shadow-xl max-w-md w-full;
|
|
}
|
|
|
|
.modal-header {
|
|
@apply flex items-center justify-between px-6 py-4 border-b border-gray-200;
|
|
}
|
|
|
|
.modal-body {
|
|
@apply px-6 py-4;
|
|
}
|
|
|
|
.modal-footer {
|
|
@apply flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200;
|
|
}
|
|
|
|
.modal-title {
|
|
@apply text-lg font-bold text-gray-900;
|
|
}
|
|
|
|
.btn-close {
|
|
@apply text-gray-400 hover:text-gray-600 transition p-1;
|
|
}
|
|
|
|
.btn-close-white {
|
|
@apply text-white hover:text-white/80;
|
|
}
|
|
|
|
.modal-header.bg-success {
|
|
@apply bg-emerald-500 text-white;
|
|
}
|
|
|
|
.modal-header.bg-danger {
|
|
@apply bg-red-500 text-white;
|
|
}
|
|
|
|
.modal-header.bg-primary {
|
|
@apply bg-temple-red text-white;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="https://unpkg.com/preact@10.19.3/dist/preact.umd.js"></script>
|
|
<script src="https://unpkg.com/htm@3.1.1/dist/htm.umd.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
|
|
|
<!-- FilePond for file uploads -->
|
|
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
|
|
<script src="https://unpkg.com/filepond/dist/filepond.js"></script>
|
|
<script src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js"></script>
|
|
<script src="https://unpkg.com/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.js"></script>
|
|
|
|
<script>
|
|
// Form data from Django
|
|
const formId = {{ form.id }};
|
|
const formStructure = {{ form.structure|safe }};
|
|
const isEmbed = {{ is_embed|yesno:"true,false" }};
|
|
|
|
const { h, Component, render, useState, useEffect, useRef } = preact;
|
|
const html = htm.bind(h);
|
|
|
|
// Field Components for Preview
|
|
class PreviewTextField extends Component {
|
|
render() {
|
|
const { field, value, onChange, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
<input type="text"
|
|
class="form-control ${error ? 'is-invalid' : ''}"
|
|
placeholder=${field.placeholder || ''}
|
|
value=${value || ''}
|
|
onInput=${(e) => onChange(field.id, e.target.value)}
|
|
required=${field.required} />
|
|
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class PreviewEmailField extends Component {
|
|
render() {
|
|
const { field, value, onChange, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
<input type="email"
|
|
class="form-control ${error ? 'is-invalid' : ''}"
|
|
placeholder=${field.placeholder || ''}
|
|
value=${value || ''}
|
|
onInput=${(e) => onChange(field.id, e.target.value)}
|
|
required=${field.required} />
|
|
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class PreviewPhoneField extends Component {
|
|
render() {
|
|
const { field, value, onChange, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
<input type="tel"
|
|
class="form-control ${error ? 'is-invalid' : ''}"
|
|
placeholder=${field.placeholder || ''}
|
|
value=${value || ''}
|
|
onInput=${(e) => onChange(field.id, e.target.value)}
|
|
required=${field.required} />
|
|
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class PreviewDateField extends Component {
|
|
render() {
|
|
const { field, value, onChange, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
<input type="date"
|
|
class="form-control ${error ? 'is-invalid' : ''}"
|
|
value=${value || ''}
|
|
onInput=${(e) => onChange(field.id, e.target.value)}
|
|
required=${field.required} />
|
|
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class PreviewFileField extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.filePondRef = null;
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.initFilePond();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.filePondRef) {
|
|
this.filePondRef.destroy();
|
|
}
|
|
}
|
|
|
|
initFilePond() {
|
|
const { field, onChange } = this.props;
|
|
const inputElement = document.getElementById(`file-upload-${field.id}`);
|
|
|
|
if (!inputElement || typeof FilePond === 'undefined') return;
|
|
|
|
this.filePondRef = FilePond.create(inputElement, {
|
|
allowMultiple: field.multiple || false,
|
|
maxFiles: field.maxFiles || 1,
|
|
maxFileSize: field.maxFileSize ? `${field.maxFileSize}MB` : '5MB',
|
|
acceptedFileTypes: field.fileTypes || ['*'],
|
|
labelIdle: 'Drag & Drop your files or <span class="filepond--label-action">Browse</span>',
|
|
credits: false,
|
|
onupdatefiles: (fileItems) => {
|
|
const files = fileItems.map(item => item.file);
|
|
onChange(field.id, files);
|
|
}
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const { field, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
<input id="file-upload-${field.id}" type="file" />
|
|
${error ? html`<div class="text-danger small mt-1">${error}</div>` : ''}
|
|
${field.fileTypes ? html`<div class="form-text">Accepted: ${field.fileTypes.join(', ')}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class PreviewDropdownField extends Component {
|
|
render() {
|
|
const { field, value, onChange, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
<select class="form-select ${error ? 'is-invalid' : ''}"
|
|
value=${value || ''}
|
|
onChange=${(e) => onChange(field.id, e.target.value)}
|
|
required=${field.required}>
|
|
<option value="">Select an option...</option>
|
|
${field.options.map(option => html`
|
|
<option value=${option.value}>${option.value}</option>
|
|
`)}
|
|
</select>
|
|
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class PreviewRadioField extends Component {
|
|
render() {
|
|
const { field, value, onChange, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
${field.options.map(option => html`
|
|
<div class="form-check">
|
|
<input class="form-check-input"
|
|
type="radio"
|
|
name=${field.id}
|
|
id=${option.id}
|
|
value=${option.value}
|
|
checked=${value === option.value}
|
|
onChange=${(e) => onChange(field.id, e.target.value)}
|
|
required=${field.required} />
|
|
<label class="form-check-label" for=${option.id}>
|
|
${option.value}
|
|
</label>
|
|
</div>
|
|
`)}
|
|
${error ? html`<div class="text-danger small mt-1">${error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class PreviewCheckboxField extends Component {
|
|
render() {
|
|
const { field, value = [], onChange, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
${field.options.map(option => html`
|
|
<div class="form-check">
|
|
<input class="form-check-input"
|
|
type="checkbox"
|
|
id=${option.id}
|
|
value=${option.value}
|
|
checked=${value.includes(option.value)}
|
|
onChange=${(e) => {
|
|
const newValue = e.target.checked
|
|
? [...value, option.value]
|
|
: value.filter(v => v !== option.value);
|
|
onChange(field.id, newValue);
|
|
}} />
|
|
<label class="form-check-label" for=${option.id}>
|
|
${option.value}
|
|
</label>
|
|
</div>
|
|
`)}
|
|
${error ? html`<div class="text-danger small mt-1">${error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
class PreviewRatingField extends Component {
|
|
render() {
|
|
const { field, value, onChange, error } = this.props;
|
|
return html`
|
|
<div class="mb-3">
|
|
<label class="form-label">
|
|
${field.label}
|
|
${field.required ? html`<span class="text-danger">*</span>` : ''}
|
|
</label>
|
|
<div class="rating-container">
|
|
${[1, 2, 3, 4, 5].map(n => html`
|
|
<span class="rating-star ${value >= n ? 'active' : ''}"
|
|
style="font-size: 24px; color: ${value >= n ? '#fbbf24' : '#ddd'}; cursor: pointer; margin-right: 5px;"
|
|
onClick=${() => onChange(field.id, n)}>
|
|
★
|
|
</span>
|
|
`)}
|
|
</div>
|
|
${error ? html`<div class="text-danger small mt-1">${error}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Field Factory
|
|
function createPreviewField(field, props) {
|
|
const fieldProps = { ...props, field };
|
|
|
|
switch (field.type) {
|
|
case 'text': return html`<${PreviewTextField} ...${fieldProps} />`;
|
|
case 'email': return html`<${PreviewEmailField} ...${fieldProps} />`;
|
|
case 'phone': return html`<${PreviewPhoneField} ...${fieldProps} />`;
|
|
case 'date': return html`<${PreviewDateField} ...${fieldProps} />`;
|
|
case 'file': return html`<${PreviewFileField} ...${fieldProps} />`;
|
|
case 'dropdown': return html`<${PreviewDropdownField} ...${fieldProps} />`;
|
|
case 'radio': return html`<${PreviewRadioField} ...${fieldProps} />`;
|
|
case 'checkbox': return html`<${PreviewCheckboxField} ...${fieldProps} />`;
|
|
case 'rating': return html`<${PreviewRatingField} ...${fieldProps} />`;
|
|
default: return html`<${PreviewTextField} ...${fieldProps} />`;
|
|
}
|
|
}
|
|
|
|
// Main Form Preview Component
|
|
class FormPreview extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
currentWizardIndex: 0,
|
|
formData: {},
|
|
errors: {},
|
|
isSubmitting: false,
|
|
isSubmitted: false
|
|
};
|
|
}
|
|
|
|
getCurrentWizard() {
|
|
return formStructure.wizards[this.state.currentWizardIndex] || null;
|
|
}
|
|
|
|
getCurrentWizardFields() {
|
|
const wizard = this.getCurrentWizard();
|
|
return wizard ? wizard.fields : [];
|
|
}
|
|
|
|
getTotalWizards() {
|
|
return formStructure.wizards ? formStructure.wizards.length : 0;
|
|
}
|
|
|
|
getProgress() {
|
|
return ((this.state.currentWizardIndex + 1) / this.getTotalWizards()) * 100;
|
|
}
|
|
|
|
validateCurrentWizard() {
|
|
const fields = this.getCurrentWizardFields();
|
|
const errors = {};
|
|
|
|
fields.forEach(field => {
|
|
const value = this.state.formData[field.id];
|
|
|
|
if (field.required && (!value || (Array.isArray(value) && value.length === 0))) {
|
|
errors[field.id] = `${field.label} is required`;
|
|
}
|
|
|
|
// Email validation
|
|
if (field.type === 'email' && value) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(value)) {
|
|
errors[field.id] = 'Please enter a valid email address';
|
|
}
|
|
}
|
|
|
|
// Phone validation
|
|
if (field.type === 'phone' && value) {
|
|
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
|
if (!phoneRegex.test(value)) {
|
|
errors[field.id] = 'Please enter a valid phone number';
|
|
}
|
|
}
|
|
});
|
|
|
|
this.setState({ errors });
|
|
return Object.keys(errors).length === 0;
|
|
}
|
|
|
|
handleFieldChange = (fieldId, value) => {
|
|
this.setState({
|
|
formData: { ...this.state.formData, [fieldId]: value },
|
|
errors: { ...this.state.errors, [fieldId]: null }
|
|
});
|
|
}
|
|
|
|
handleNext = () => {
|
|
if (this.validateCurrentWizard()) {
|
|
if (this.state.currentWizardIndex < this.getTotalWizards() - 1) {
|
|
this.setState({ currentWizardIndex: this.state.currentWizardIndex + 1 });
|
|
} else {
|
|
this.handleSubmit();
|
|
}
|
|
}
|
|
}
|
|
|
|
handlePrevious = () => {
|
|
if (this.state.currentWizardIndex > 0) {
|
|
this.setState({ currentWizardIndex: this.state.currentWizardIndex - 1 });
|
|
}
|
|
}
|
|
|
|
handleSubmit = async () => {
|
|
if (!this.validateCurrentWizard()) return;
|
|
|
|
this.setState({ isSubmitting: true });
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
|
|
// Add form data
|
|
Object.keys(this.state.formData).forEach(key => {
|
|
const value = this.state.formData[key];
|
|
if (Array.isArray(value)) {
|
|
value.forEach((file, index) => {
|
|
formData.append(`${key}_${index}`, file);
|
|
});
|
|
} else {
|
|
formData.append(key, value);
|
|
}
|
|
});
|
|
|
|
// Add CSRF token
|
|
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]');
|
|
if (csrfToken) {
|
|
formData.append('csrfmiddlewaretoken', csrfToken.value);
|
|
}
|
|
|
|
const response = await fetch(`/recruitment/forms/${formId}/submit/`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.setState({ isSubmitted: true });
|
|
openModal('successModal');
|
|
} else {
|
|
throw new Error(result.error || 'Submission failed');
|
|
}
|
|
} catch (error) {
|
|
console.error('Submission error:', error);
|
|
document.getElementById('errorMessage').textContent = error.message;
|
|
openModal('errorModal');
|
|
} finally {
|
|
this.setState({ isSubmitting: false });
|
|
}
|
|
}
|
|
|
|
resetForm = () => {
|
|
this.setState({
|
|
currentWizardIndex: 0,
|
|
formData: {},
|
|
errors: {},
|
|
isSubmitting: false,
|
|
isSubmitted: false
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const currentWizard = this.getCurrentWizard();
|
|
const currentFields = this.getCurrentWizardFields();
|
|
|
|
if (this.state.isSubmitted) {
|
|
return html`
|
|
<div class="text-center py-12">
|
|
<div class="inline-flex items-center justify-center w-16 h-16 bg-emerald-100 rounded-full mb-4">
|
|
<i data-lucide="check-circle" class="w-8 h-8 text-emerald-600"></i>
|
|
</div>
|
|
<h3 class="text-2xl font-bold text-gray-900 mb-2">Thank You!</h3>
|
|
<p class="text-gray-500 mb-6">Your form has been submitted successfully.</p>
|
|
<button class="btn btn-primary" onClick=${this.resetForm}>
|
|
Submit Another Response
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html`
|
|
<div class="form-preview p-6">
|
|
<!-- Progress Bar -->
|
|
${formStructure.settings && formStructure.settings.showProgress && this.getTotalWizards() > 1 ? html`
|
|
<div class="progress mb-6" style="height: 6px;">
|
|
<div class="progress-bar"
|
|
style="width: ${this.getProgress()}%; transition: width 0.3s ease;">
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- Wizard Title -->
|
|
${currentWizard ? html`
|
|
<h4 class="text-xl font-bold text-gray-900 mb-6">${currentWizard.title || 'Step ' + (this.state.currentWizardIndex + 1)}</h4>
|
|
` : ''}
|
|
|
|
<!-- Form Fields -->
|
|
<form id="preview-form">
|
|
${currentFields.map(field =>
|
|
createPreviewField(field, {
|
|
key: field.id,
|
|
value: this.state.formData[field.id],
|
|
onChange: this.handleFieldChange,
|
|
error: this.state.errors[field.id]
|
|
})
|
|
)}
|
|
</form>
|
|
|
|
<!-- Navigation Buttons -->
|
|
<div class="flex flex-col sm:flex-row justify-between items-center gap-3 mt-6">
|
|
<button type="button"
|
|
class="btn btn-outline-secondary w-full sm:w-auto"
|
|
onClick=${this.handlePrevious}
|
|
disabled=${this.state.currentWizardIndex === 0 || this.state.isSubmitting}>
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i> Previous
|
|
</button>
|
|
|
|
<button type="button"
|
|
class="btn btn-primary w-full sm:w-auto"
|
|
onClick=${this.handleNext}
|
|
disabled=${this.state.isSubmitting}>
|
|
${this.state.isSubmitting ? html`
|
|
<span class="spinner-border spinner-border-sm mr-2"></span>
|
|
Submitting...
|
|
` : ''}
|
|
${this.state.currentWizardIndex === this.getTotalWizards() - 1 ?
|
|
html`<i data-lucide="check" class="w-4 h-4"></i> Submit` :
|
|
html`Next <i data-lucide="arrow-right" class="w-4 h-4"></i>`
|
|
}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Modal functions
|
|
function openModal(modalId) {
|
|
const modal = document.getElementById(modalId);
|
|
if (modal) {
|
|
modal.classList.remove('hidden');
|
|
setTimeout(() => {
|
|
const backdrop = modal.querySelector('.absolute');
|
|
if (backdrop) backdrop.classList.remove('opacity-0');
|
|
const content = modal.querySelector('.relative.bg-white');
|
|
if (content) {
|
|
content.classList.remove('opacity-0', 'scale-95');
|
|
content.classList.add('opacity-100', 'scale-100');
|
|
}
|
|
}, 10);
|
|
}
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
const modal = document.getElementById(modalId);
|
|
if (modal) {
|
|
const backdrop = modal.querySelector('.absolute');
|
|
if (backdrop) backdrop.classList.add('opacity-0');
|
|
const content = modal.querySelector('.relative.bg-white');
|
|
if (content) {
|
|
content.classList.remove('opacity-100', 'scale-100');
|
|
content.classList.add('opacity-0', 'scale-95');
|
|
}
|
|
setTimeout(() => {
|
|
modal.classList.add('hidden');
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
// Initialize FilePond plugins
|
|
if (typeof FilePond !== 'undefined') {
|
|
FilePond.registerPlugin(
|
|
FilePondPluginFileValidateType,
|
|
FilePondPluginFileValidateSize
|
|
);
|
|
}
|
|
|
|
// Render form preview
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const container = document.getElementById('form-preview-container');
|
|
if (container) {
|
|
render(html`<${FormPreview} />`, container);
|
|
lucide.createIcons();
|
|
}
|
|
});
|
|
|
|
// Global function for modal reset
|
|
window.resetForm = function() {
|
|
const container = document.getElementById('form-preview-container');
|
|
if (container) {
|
|
render(html`<${FormPreview} />`, container);
|
|
lucide.createIcons();
|
|
}
|
|
};
|
|
|
|
window.closeModal = closeModal;
|
|
window.openModal = openModal;
|
|
</script>
|
|
{% endblock %} |