kaauh_ats/templates/forms/edit_form.html
2025-10-05 12:19:45 +03:00

1406 lines
44 KiB
HTML

{% extends "base.html" %}
{% block title %}Edit Form - {{ form.title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Edit Mode Header -->
<div class="alert alert-info d-flex align-items-center mb-4">
<i class="fas fa-edit me-2"></i>
<div>
<strong>Edit Mode:</strong> You are currently editing "{{ form.title }}"
<br>
<small class="text-muted">Last updated: {{ form.updated_at|date:"M d, Y g:i A" }}</small>
</div>
<div class="ms-auto">
<a href="{% url 'form_preview' form.id %}" class="btn btn-outline-primary btn-sm me-2" target="_blank">
<i class="fas fa-eye"></i> Preview
</a>
<a href="{% url 'form_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-arrow-left"></i> Back to Forms
</a>
</div>
</div>
<!-- Form Builder Container -->
<div id="form-builder-app"></div>
</div>
</div>
</div>
{% endblock %}
<!-- Form Data Script -->
<script type="application/json" id="form-data">
{
"id": {{ form.id|safe }},
"title": {{ form.title|safe }},
"description": {{ form.description|safe }},
"structure": {{ form.structure|safe }},
"created_at": "{{ form.created_at|date:'c' }}",
"updated_at": "{{ form.updated_at|date:'c' }}"
}
</script>
<!-- Enhanced Form Builder for Edit Mode -->
<!-- Import required libraries -->
<script type="module" src="https://unpkg.com/preact@10.19.6/dist/preact.module.js"></script>
<script type="module" src="https://unpkg.com/htm@3.1.1/dist/htm.module.js"></script>
<script type="module">
const { h, Component, render, useState, useEffect, useRef } = preact;
const html = htm.bind(h);
// Field Types Configuration (same as original)
const FIELD_TYPES = [
{ type: 'text', name: 'Text Input', icon: 'T' },
{ type: 'email', name: 'Email Input', icon: '@' },
{ type: 'phone', name: 'Phone Input', icon: '📞' },
{ type: 'date', name: 'Date Picker', icon: '📅' },
{ type: 'file', name: 'File Upload', icon: '📎' },
{ type: 'dropdown', name: 'Dropdown', icon: '▼' },
{ type: 'radio', name: 'Radio Buttons', icon: '○' },
{ type: 'checkbox', name: 'Checkboxes', icon: '☐' },
{ type: 'rating', name: 'Rating', icon: '⭐' }
];
// Field Components (same as original)
class BaseField extends Component {
render() {
const { field, onSelect, onRemove, onMoveUp, onMoveDown, isSelected, index, totalFields } = this.props;
return html`
<div class="form-field ${isSelected ? 'selected' : ''}" onClick=${() => onSelect(field.id)}>
<div class="field-header">
<div class="field-label">
<span>${field.label || 'Untitled Field'}</span>
${field.required ? html`<span class="required-star">*</span>` : ''}
</div>
<div class="field-actions">
<button class="field-action" onClick=${(e) => { e.stopPropagation(); onMoveUp(index); }} disabled=${index === 0}>
⬆️
</button>
<button class="field-action" onClick=${(e) => { e.stopPropagation(); onMoveDown(index); }} disabled=${index === totalFields - 1}>
⬇️
</button>
<button class="field-action" onClick=${(e) => { e.stopPropagation(); onRemove(index); }}>
🗑️
</button>
</div>
</div>
${this.renderFieldContent()}
</div>
`;
}
}
class TextField extends BaseField {
renderFieldContent() {
return html`<input type="text" class="field-input" placeholder=${this.props.field.placeholder || ''} />`;
}
}
class EmailField extends BaseField {
renderFieldContent() {
return html`<input type="email" class="field-input" placeholder=${this.props.field.placeholder || ''} />`;
}
}
class PhoneField extends BaseField {
renderFieldContent() {
return html`<input type="tel" class="field-input" placeholder=${this.props.field.placeholder || ''} />`;
}
}
class DateField extends BaseField {
renderFieldContent() {
return html`<input type="date" class="field-input" />`;
}
}
class FileField extends BaseField {
constructor(props) {
super(props);
this.filePondRef = null;
}
componentDidMount() {
this.initFilePond();
}
componentWillUnmount() {
if (this.filePondRef) {
this.filePondRef.destroy();
}
}
initFilePond() {
const { field } = this.props;
const inputElement = document.getElementById(`file-upload-${field.id}`);
if (!inputElement || typeof FilePond === 'undefined') return;
const acceptedFileTypes = field.fileTypesString
? field.fileTypesString.split(',').map(type => type.trim())
: ['*'];
this.filePondRef = FilePond.create(inputElement, {
allowMultiple: field.multiple || false,
maxFiles: field.maxFiles || 1,
maxFileSize: field.maxFileSize ? `${field.maxFileSize}MB` : '5MB',
acceptedFileTypes: acceptedFileTypes,
labelIdle: 'Drag & Drop your files or <span class="filepond--label-action">Browse</span>',
credits: false
});
}
renderFieldContent() {
const { field } = this.props;
return html`
<div class="file-upload-container">
<input id="file-upload-${field.id}" type="file" />
</div>
<div class="file-upload-info">
${field.fileTypes ? html`<div>Accepted file types: ${field.fileTypes.join(', ')}</div>` : ''}
${field.maxFileSize ? html`<div>Maximum file size: ${field.maxFileSize} MB</div>` : ''}
${field.maxFiles ? html`<div>Maximum files: ${field.maxFiles}</div>` : ''}
</div>
`;
}
}
class DropdownField extends BaseField {
renderFieldContent() {
const { field } = this.props;
return html`
<select class="field-input">
${field.options.map(option => html`<option>${option.value}</option>`)}
</select>
`;
}
}
class RadioField extends BaseField {
renderFieldContent() {
const { field } = this.props;
return html`
<div class="field-options">
${field.options.map(option => html`
<div class="option-item">
<input type="radio" name=${field.id} id=${option.id} />
<label for=${option.id}>${option.value}</label>
</div>
`)}
</div>
`;
}
}
class CheckboxField extends BaseField {
renderFieldContent() {
const { field } = this.props;
return html`
<div class="field-options">
${field.options.map(option => html`
<div class="option-item">
<input type="checkbox" id=${option.id} />
<label for=${option.id}>${option.value}</label>
</div>
`)}
</div>
`;
}
}
class RatingField extends BaseField {
renderFieldContent() {
return html`
<div style="display: flex; gap: 5px;">
${[1, 2, 3, 4, 5].map(n => html`
<span style="font-size: 24px; color: #ffc107; cursor: pointer;">★</span>
`)}
</div>
`;
}
}
// Field Factory
function createFieldComponent(field, props) {
const fieldProps = { ...props, field };
switch (field.type) {
case 'text': return html`<${TextField} ...${fieldProps} />`;
case 'email': return html`<${EmailField} ...${fieldProps} />`;
case 'phone': return html`<${PhoneField} ...${fieldProps} />`;
case 'date': return html`<${DateField} ...${fieldProps} />`;
case 'file': return html`<${FileField} ...${fieldProps} />`;
case 'dropdown': return html`<${DropdownField} ...${fieldProps} />`;
case 'radio': return html`<${RadioField} ...${fieldProps} />`;
case 'checkbox': return html`<${CheckboxField} ...${fieldProps} />`;
case 'rating': return html`<${RatingField} ...${fieldProps} />`;
default: return html`<${TextField} ...${fieldProps} />`;
}
}
// Enhanced Form Builder Component for Edit Mode
class EditFormBuilder extends Component {
constructor(props) {
super(props);
// Load form data from the page with error handling
const formDataElement = document.getElementById('form-data');
let formData;
try {
formData = JSON.parse(formDataElement.textContent);
// Validate form data structure
if (!formData.structure || !formData.structure.wizards) {
throw new Error('Invalid form data structure');
}
} catch (error) {
console.error('Error loading form data:', error);
alert('Error loading form data. Please refresh the page or contact support.');
return;
}
this.state = {
form: formData.structure,
originalForm: JSON.parse(JSON.stringify(formData.structure)), // Deep copy for comparison
formId: formData.id,
isDirty: false,
isSaving: false,
isDragOver: false,
selectedFieldId: null,
currentWizardId: formData.structure.wizards?.[0]?.id || 'wizard-1',
previewWizardIndex: 0,
previewProgress: 30,
showPreview: false,
showAnalytics: false
};
this.canvasRef = null;
this.sortableInstance = null;
}
componentDidMount() {
this.initializeDragAndDrop();
// Register FilePond plugins if available
if (typeof FilePond !== 'undefined') {
FilePond.registerPlugin(
FilePondPluginImagePreview,
FilePondPluginFileValidateType,
FilePondPluginFileValidateSize
);
}
}
componentDidUpdate() {
// Reinitialize sortable when fields change
if (this.canvasRef && !this.sortableInstance) {
this.initializeDragAndDrop();
}
}
initializeDragAndDrop() {
if (!this.canvasRef || typeof Sortable === 'undefined') return;
this.sortableInstance = Sortable.create(this.canvasRef, {
animation: 150,
handle: '.form-field',
onEnd: (evt) => {
const { oldIndex, newIndex } = evt;
const currentWizard = this.getCurrentWizard();
const field = currentWizard.fields[oldIndex];
currentWizard.fields.splice(oldIndex, 1);
currentWizard.fields.splice(newIndex, 0, field);
this.setState({ isDirty: true });
}
});
}
getCurrentWizard() {
return this.state.form.wizards.find(wizard => wizard.id === this.state.currentWizardId) || null;
}
getCurrentWizardFields() {
const wizard = this.getCurrentWizard();
return wizard ? wizard.fields : [];
}
getSelectedField() {
for (const wizard of this.state.form.wizards) {
const field = wizard.fields.find(field => field.id === this.state.selectedFieldId);
if (field) return field;
}
return null;
}
getTotalFieldsCount() {
return this.state.form.wizards.reduce((total, wizard) => total + wizard.fields.length, 0);
}
getRequiredFieldsCount() {
let count = 0;
for (const wizard of this.state.form.wizards) {
count += wizard.fields.filter(field => field.required).length;
}
return count;
}
getCurrentPreviewWizard() {
return this.state.form.wizards[this.state.previewWizardIndex] || this.state.form.wizards[0];
}
onDragStart = (e, fieldType) => {
e.dataTransfer.setData('text/plain', fieldType);
}
onDragOver = (e) => {
e.preventDefault();
this.setState({ isDragOver: true });
}
onDragLeave = () => {
this.setState({ isDragOver: false });
}
onDrop = (e) => {
e.preventDefault();
this.setState({ isDragOver: false });
const fieldType = e.dataTransfer.getData('text/plain');
this.addField(fieldType);
}
addField = (type) => {
const field = {
id: 'field-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9),
type: type,
label: this.getDefaultLabel(type),
placeholder: this.getDefaultPlaceholder(type),
required: false,
wizardId: this.state.currentWizardId,
options: type === 'dropdown' || type === 'radio' || type === 'checkbox'
? [{ id: 'opt-1', value: 'Option 1' }, { id: 'opt-2', value: 'Option 2' }]
: [],
validation: {
minLength: null,
maxLength: null
},
styling: {}
};
// File upload specific properties
if (type === 'file') {
field.fileTypes = ['image/*', '.pdf', '.doc', '.docx'];
field.fileTypesString = 'image/*, .pdf, .doc, .docx';
field.maxFileSize = 5;
field.maxFiles = 1;
field.multiple = false;
}
const currentWizard = this.getCurrentWizard();
currentWizard.fields.push(field);
this.setState({ selectedFieldId: field.id, isDirty: true });
}
getDefaultLabel(type) {
const labels = {
text: 'Text Input',
email: 'Email Address',
phone: 'Phone Number',
date: 'Select Date',
file: 'Upload Files',
dropdown: 'Select Option',
radio: 'Choose One',
checkbox: 'Select Options',
rating: 'Rate Your Experience'
};
return labels[type] || 'Field Label';
}
getDefaultPlaceholder(type) {
const placeholders = {
text: 'Enter text here...',
email: 'your.email@example.com',
phone: '(123) 456-7890',
date: '',
file: '',
dropdown: '',
radio: '',
checkbox: '',
rating: ''
};
return placeholders[type] || '';
}
selectField = (fieldId) => {
this.setState({ selectedFieldId: fieldId });
}
removeField = (index) => {
const currentWizard = this.getCurrentWizard();
currentWizard.fields.splice(index, 1);
if (currentWizard.fields[index - 1]) {
this.setState({ selectedFieldId: currentWizard.fields[index - 1].id, isDirty: true });
} else {
this.setState({ selectedFieldId: null, isDirty: true });
}
}
moveFieldUp = (index) => {
if (index > 0) {
const currentWizard = this.getCurrentWizard();
const field = currentWizard.fields[index];
currentWizard.fields.splice(index, 1);
currentWizard.fields.splice(index - 1, 0, field);
this.setState({ isDirty: true });
}
}
moveFieldDown = (index) => {
const currentWizard = this.getCurrentWizard();
if (index < currentWizard.fields.length - 1) {
const field = currentWizard.fields[index];
currentWizard.fields.splice(index, 1);
currentWizard.fields.splice(index + 1, 0, field);
this.setState({ isDirty: true });
}
}
addWizard = () => {
const newWizard = {
id: 'wizard-' + Date.now(),
title: `Step ${this.state.form.wizards.length + 1}`,
fields: []
};
this.state.form.wizards.push(newWizard);
this.setState({ currentWizardId: newWizard.id, isDirty: true });
}
removeWizard = (index) => {
if (this.state.form.wizards.length > 1) {
this.state.form.wizards.splice(index, 1);
this.setState({ currentWizardId: this.state.form.wizards[0].id, isDirty: true });
}
}
selectWizard = (wizardId) => {
this.setState({ currentWizardId: wizardId, selectedFieldId: null });
}
updateFormTitle = (e) => {
this.setState({
form: { ...this.state.form, title: e.target.value },
isDirty: true
});
}
updateFormDescription = (e) => {
this.setState({
form: { ...this.state.form, description: e.target.value },
isDirty: true
});
}
updateWizardTitle = (e) => {
const currentWizard = this.getCurrentWizard();
if (currentWizard) {
currentWizard.title = e.target.value;
this.setState({ isDirty: true });
}
}
updateForm = async () => {
try {
this.setState({ isSaving: true });
const response = await fetch(`/api/forms/${this.state.formId}/update/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': (() => {
const cookieName = 'csrftoken';
const name = cookieName + '=';
const ca = document.cookie.split(';');
for (const c of ca) {
if (c.trim().startsWith(name)) {
return c.substring(name.length, c.length);
}
}
return '';
})()
},
body: JSON.stringify({
form: this.state.form
})
});
const result = await response.json();
if (result.success) {
this.setState({
isSaving: false,
isDirty: false,
originalForm: JSON.parse(JSON.stringify(this.state.form)) // Update original form
});
this.showNotification('Form updated successfully!', 'success');
} else {
throw new Error(result.error || 'Failed to update form');
}
} catch (error) {
console.error('Error updating form:', error);
this.setState({ isSaving: false });
this.showNotification('Error updating form: ' + error.message, 'error');
}
}
showNotification = (message, type = 'info') => {
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
}
openPreview = () => {
window.open(`/forms/${this.state.formId}/`, '_blank');
}
togglePreview = () => {
this.setState({
showPreview: !this.state.showPreview,
previewWizardIndex: 0
});
this.updatePreviewProgress();
}
updatePreviewProgress = () => {
const progress = ((this.state.previewWizardIndex + 1) / this.state.form.wizards.length) * 100;
this.setState({ previewProgress: progress });
}
render() {
const currentWizard = this.getCurrentWizard();
const currentWizardFields = this.getCurrentWizardFields();
const selectedField = this.getSelectedField();
return html`
<div class="container">
<!-- Header -->
<header>
<div class="logo">
<div class="logo-icon">FB</div>
<span>Form Builder - Edit Mode</span>
</div>
<div class="form-meta">
<input type="text" class="meta-input" placeholder="Form Title" value=${this.state.form.title} onInput=${this.updateFormTitle} />
<input type="text" class="meta-input" placeholder="Form Description" value=${this.state.form.description} onInput=${this.updateFormDescription} />
</div>
<div class="header-actions">
<button class="btn btn-outline" onClick=${this.updateForm} disabled=${this.state.isSaving || !this.state.isDirty}>
${this.state.isSaving ? html`<span class="spinner-border spinner-border-sm me-2"></span>Updating...` : html`💾 Update`}
</button>
<button class="btn btn-outline" onClick=${this.openPreview}>
👁️ Preview
</button>
<a href="{% url 'form_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back
</a>
</div>
</header>
<!-- Main Content -->
<div class="main-content">
<!-- Left Sidebar - Field Palette -->
<div class="sidebar">
<h3 class="sidebar-title">Field Types</h3>
<div class="field-types">
${FIELD_TYPES.map(fieldType => html`
<div class="field-type"
draggable="true"
onDragStart=${(e) => this.onDragStart(e, fieldType.type)}>
<div class="field-icon">${fieldType.icon}</div>
<span class="field-name">${fieldType.name}</span>
</div>
`)}
</div>
<!-- Wizard Management -->
<div class="wizard-manager">
<h3 class="sidebar-title">Form Steps</h3>
<div class="wizard-list">
${this.state.form.wizards.map((wizard, index) => html`
<div class="wizard-item ${wizard.id === this.state.currentWizardId ? 'active' : ''}"
onClick=${() => this.selectWizard(wizard.id)}>
<span>${wizard.title || 'Untitled Step'}</span>
<div class="wizard-actions">
<button class="wizard-action"
onClick=${(e) => { e.stopPropagation(); this.removeWizard(index); }}
disabled=${this.state.form.wizards.length <= 1}>
🗑️
</button>
</div>
</div>
`)}
</div>
<button class="btn btn-outline" onClick=${this.addWizard} style="width: 100%;">
Add Step
</button>
</div>
</div>
<!-- Center Canvas -->
<div class="canvas-container">
<div class="canvas" ref=${el => this.canvasRef = el}>
${currentWizard ? html`
<div class="wizard-header">
<div class="wizard-title">${currentWizard.title || 'Untitled Step'}</div>
<input type="text" class="meta-input" placeholder="Step Title" value=${currentWizard.title || ''} onInput=${this.updateWizardTitle} />
</div>
` : ''}
<div class="drop-zone ${this.state.isDragOver ? 'active' : ''}"
onDragOver=${this.onDragOver}
onDragLeave=${this.onDragLeave}
onDrop=${this.onDrop}>
Drag and drop fields here to build your form
</div>
${currentWizardFields.map((field, index) =>
createFieldComponent(field, {
key: field.id,
onSelect: this.selectField,
onRemove: this.removeField,
onMoveUp: this.moveFieldUp,
onMoveDown: this.moveFieldDown,
isSelected: this.state.selectedFieldId === field.id,
index: index,
totalFields: currentWizardFields.length
})
)}
</div>
</div>
<!-- Right Sidebar - Property Editor -->
<div class="property-editor" style=${selectedField ? '' : 'display: none;'}>
<h3 class="sidebar-title">Field Properties</h3>
<div class="property-section">
<h4 class="property-title">Basic</h4>
<div class="property-item">
<label class="property-label">Label</label>
<input type="text" class="property-input" value=${selectedField?.label || ''}
onInput=${(e) => { if (selectedField) selectedField.label = e.target.value; this.setState({ isDirty: true }); }} />
</div>
<div class="property-item">
<label class="property-label">Placeholder</label>
<input type="text" class="property-input" value=${selectedField?.placeholder || ''}
onInput=${(e) => { if (selectedField) selectedField.placeholder = e.target.value; this.setState({ isDirty: true }); }} />
</div>
<div class="property-item">
<div class="checkbox-group">
<input type="checkbox" id="required" checked=${selectedField?.required || false}
onChange=${(e) => { if (selectedField) selectedField.required = e.target.checked; this.setState({ isDirty: true }); }} />
<label for="required" class="property-label">Required Field</label>
</div>
</div>
</div>
<div class="property-section">
<button class="btn btn-outline" onClick=${() => {
const currentWizard = this.getCurrentWizard();
const index = currentWizard.fields.findIndex(f => f.id === this.state.selectedFieldId);
if (index !== -1) {
this.removeField(index);
}
}} style="width: 100%;">
Delete Field
</button>
</div>
</div>
</div>
<!-- Footer -->
<footer>
<div class="form-stats">
<div class="stat-item">
<span>Steps:</span>
<strong>${this.state.form.wizards.length}</strong>
</div>
<div class="stat-item">
<span>Fields:</span>
<strong>${this.getTotalFieldsCount()}</strong>
</div>
<div class="stat-item">
<span>Required:</span>
<strong>${this.getRequiredFieldsCount()}</strong>
</div>
${this.state.isDirty ? html`
<div class="stat-item">
<span class="badge bg-warning">Unsaved Changes</span>
</div>
` : ''}
</div>
<div>
<button class="btn btn-outline" onClick=${() => alert('Background customization panel would open here')}>
Customize Background
</button>
</div>
</footer>
</div>
${this.state.showPreview ? this.renderPreviewMode() : ''}
`;
}
renderPreviewMode() {
const currentPreviewWizard = this.getCurrentPreviewWizard();
return html`
<div class="preview-mode">
<div class="preview-header">
<h2>${this.state.form.title || 'Form Preview'}</h2>
<button class="btn btn-outline" onClick=${this.togglePreview}>
✖️ Close Preview
</button>
</div>
<div class="preview-content">
<p>${this.state.form.description || 'Form description'}</p>
<div class="progress-bar">
<div class="progress-fill" style="width: ${this.state.previewProgress}%"></div>
</div>
<div class="wizard-title">${currentPreviewWizard.title || 'Step'}</div>
${currentPreviewWizard.fields.map(field =>
createFieldComponent(field, {
key: field.id,
onSelect: () => {},
onRemove: () => {},
onMoveUp: () => {},
onMoveDown: () => {},
isSelected: false,
index: 0,
totalFields: 0
})
)}
<div class="wizard-navigation">
<button class="btn btn-outline" disabled=${this.state.previewWizardIndex === 0}>
Previous
</button>
<div>
Step ${this.state.previewWizardIndex + 1} of ${this.state.form.wizards.length}
</div>
<button class="btn btn-primary">
${this.state.previewWizardIndex === this.state.form.wizards.length - 1 ? 'Submit' : 'Next'}
</button>
</div>
</div>
</div>
`;
}
}
// Render the edit form builder
render(html`<${EditFormBuilder} />`, document.getElementById('form-builder-app'));
</script>
<style>
/* Include all the same styles as the original form builder */
:root {
--primary: #4361ee;
--primary-light: #4895ef;
--secondary: #3f37c9;
--success: #4cc9f0;
--danger: #f72585;
--warning: #f8961e;
--info: #4895ef;
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--gray-light: #e9ecef;
--border-radius: 8px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f7fb;
color: var(--dark);
line-height: 1.6;
}
.container {
display: grid;
grid-template-rows: 60px 1fr 40px;
height: 100vh;
overflow: hidden;
}
/* Header Styles */
header {
background: white;
border-bottom: 1px solid var(--gray-light);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-shadow: var(--shadow);
z-index: 10;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 1.2rem;
color: var(--primary);
}
.logo-icon {
width: 32px;
height: 32px;
background: var(--primary);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.form-meta {
display: flex;
gap: 15px;
}
.meta-input {
border: 1px solid var(--gray-light);
border-radius: var(--border-radius);
padding: 6px 12px;
font-size: 0.9rem;
}
.header-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 8px 16px;
border-radius: var(--border-radius);
border: none;
cursor: pointer;
font-weight: 500;
transition: var(--transition);
display: flex;
align-items: center;
gap: 6px;
text-decoration: none;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--secondary);
}
.btn-outline {
background: transparent;
border: 1px solid var(--primary);
color: var(--primary);
}
.btn-outline:hover {
background: var(--primary);
color: white;
}
.btn-outline-secondary {
background: transparent;
border: 1px solid var(--gray);
color: var(--gray);
}
.btn-outline-secondary:hover {
background: var(--gray);
color: white;
}
/* Main Content */
.main-content {
display: grid;
grid-template-columns: 250px 1fr 300px;
height: 100%;
overflow: hidden;
}
/* Sidebar Styles */
.sidebar {
background: white;
border-right: 1px solid var(--gray-light);
padding: 20px;
overflow-y: auto;
}
.sidebar-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 15px;
color: var(--dark);
}
.field-types {
display: grid;
gap: 10px;
}
.field-type {
background: var(--light);
border: 1px solid var(--gray-light);
border-radius: var(--border-radius);
padding: 12px;
cursor: grab;
transition: var(--transition);
display: flex;
align-items: center;
gap: 10px;
}
.field-type:hover {
background: white;
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.field-icon {
width: 24px;
height: 24px;
background: var(--primary-light);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.8rem;
}
.field-name {
font-weight: 500;
font-size: 0.9rem;
}
/* Wizard Management */
.wizard-manager {
margin-top: 20px;
border-top: 1px solid var(--gray-light);
padding-top: 20px;
}
.wizard-list {
display: grid;
gap: 8px;
margin-bottom: 15px;
}
.wizard-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--light);
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
}
.wizard-item.active {
background: var(--primary);
color: white;
}
.wizard-item:hover:not(.active) {
background: #e9ecef;
}
.wizard-actions {
display: flex;
gap: 5px;
}
.wizard-action {
background: none;
border: none;
cursor: pointer;
font-size: 0.8rem;
opacity: 0.7;
}
.wizard-action:hover {
opacity: 1;
}
/* Canvas Styles */
.canvas-container {
padding: 20px;
overflow-y: auto;
background: #f9fafc;
}
.canvas {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
min-height: 600px;
padding: 30px;
position: relative;
}
.wizard-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--gray-light);
}
.wizard-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--primary);
}
.drop-zone {
border: 2px dashed var(--gray-light);
border-radius: var(--border-radius);
padding: 40px;
text-align: center;
color: var(--gray);
margin-bottom: 20px;
transition: var(--transition);
}
.drop-zone.active {
border-color: var(--primary);
background: rgba(67, 97, 238, 0.05);
}
.form-field {
background: white;
border: 1px solid var(--gray-light);
border-radius: var(--border-radius);
padding: 20px;
margin-bottom: 20px;
transition: var(--transition);
position: relative;
}
.form-field:hover {
border-color: var(--primary);
}
.form-field.selected {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.2);
}
.field-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.field-label {
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
gap: 5px;
}
.required-star {
color: var(--danger);
font-size: 1.2rem;
}
.field-actions {
display: flex;
gap: 8px;
}
.field-action {
background: none;
border: none;
cursor: pointer;
color: var(--gray);
font-size: 0.9rem;
transition: var(--transition);
}
.field-action:hover {
color: var(--primary);
}
.field-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--gray-light);
border-radius: var(--border-radius);
font-size: 1rem;
}
.field-input:focus {
outline: none;
border-color: var(--primary);
}
.field-options {
display: grid;
gap: 8px;
margin-top: 10px;
}
.option-item {
display: flex;
align-items: center;
gap: 10px;
}
.option-input {
flex: 1;
padding: 8px 12px;
border: 1px solid var(--gray-light);
border-radius: var(--border-radius);
}
/* File Upload Field Styling */
.file-upload-container {
margin-top: 10px;
}
.file-upload-info {
margin-top: 10px;
font-size: 0.85rem;
color: var(--gray);
}
/* Property Editor Styles */
.property-editor {
background: white;
border-left: 1px solid var(--gray-light);
padding: 20px;
overflow-y: auto;
}
.property-section {
margin-bottom: 25px;
}
.property-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 15px;
color: var(--dark);
display: flex;
align-items: center;
gap: 8px;
}
.property-item {
margin-bottom: 15px;
}
.property-label {
display: block;
margin-bottom: 6px;
font-size: 0.9rem;
font-weight: 500;
}
.property-input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--gray-light);
border-radius: var(--border-radius);
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
/* Footer Styles */
footer {
background: white;
border-top: 1px solid var(--gray-light);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
font-size: 0.9rem;
color: var(--gray);
}
.form-stats {
display: flex;
gap: 20px;
}
.stat-item {
display: flex;
align-items: center;
gap: 6px;
}
/* Preview Mode */
.preview-mode {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
z-index: 100;
overflow-y: auto;
display: none;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid var(--gray-light);
}
.preview-content {
max-width: 700px;
margin: 0 auto;
padding: 40px 20px;
}
.preview-field {
margin-bottom: 30px;
}
.progress-bar {
height: 6px;
background: var(--gray-light);
border-radius: 3px;
margin: 20px 0;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--primary);
transition: width 0.5s ease;
}
.wizard-navigation {
display: flex;
gap: 10px;
margin-top: 20px;
justify-content: space-between;
}
/* Responsive Adjustments */
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 220px 1fr 280px;
}
}
@media (max-width: 992px) {
.main-content {
grid-template-columns: 200px 1fr;
}
.property-editor {
display: none;
}
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
.canvas-container {
padding: 10px;
}
.canvas {
min-height: 400px;
padding: 15px;
}
.header-actions {
flex-wrap: wrap;
gap: 5px;
}
.btn {
padding: 6px 12px;
font-size: 0.9rem;
}
.logo {
font-size: 1rem;
}
.logo-icon {
width: 24px;
height: 24px;
}
.form-meta {
flex-direction: column;
gap: 10px;
width: 100%;
}
.meta-input {
width: 100%;
}
.field-types {
grid-template-columns: 1fr;
}
.wizard-manager {
display: none;
}
.field-types {
grid-template-columns: 1fr;
}
}
</style>