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

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 %}