1605 lines
59 KiB
HTML
1605 lines
59 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dynamic Form Builder</title>
|
|
|
|
<!-- Preact and HTM for JSX without build step -->
|
|
<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>
|
|
|
|
<!-- FilePond CSS -->
|
|
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
|
|
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
|
|
<!-- FilePond JS -->
|
|
<script src="https://unpkg.com/filepond/dist/filepond.js"></script>
|
|
<script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.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>
|
|
|
|
<style>
|
|
: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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* Analytics Dashboard */
|
|
.analytics-dashboard {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: white;
|
|
z-index: 100;
|
|
overflow-y: auto;
|
|
display: none;
|
|
}
|
|
|
|
.analytics-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 20px;
|
|
border-bottom: 1px solid var(--gray-light);
|
|
}
|
|
|
|
.analytics-content {
|
|
padding: 20px;
|
|
}
|
|
|
|
.metric-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.metric-card {
|
|
background: white;
|
|
border-radius: var(--border-radius);
|
|
box-shadow: var(--shadow);
|
|
padding: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.metric-label {
|
|
font-size: 0.9rem;
|
|
color: var(--gray);
|
|
}
|
|
|
|
.chart-container {
|
|
background: white;
|
|
border-radius: var(--border-radius);
|
|
box-shadow: var(--shadow);
|
|
padding: 20px;
|
|
margin-bottom: 30px;
|
|
height: 400px;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
|
|
<script type="module">
|
|
const { h, Component, render, useState, useEffect, useRef } = preact;
|
|
const html = htm.bind(h);
|
|
|
|
// Field Types Configuration
|
|
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
|
|
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} />`;
|
|
}
|
|
}
|
|
|
|
// Main Form Builder Component
|
|
class FormBuilder extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
form: {
|
|
id: "form-" + Date.now(),
|
|
title: "My Form",
|
|
description: "Form description goes here",
|
|
background: {
|
|
type: "color",
|
|
value: "#ffffff"
|
|
},
|
|
logo: null,
|
|
wizards: [
|
|
{ id: 'wizard-1', title: 'Step 1', fields: [] }
|
|
],
|
|
settings: {
|
|
oneQuestionAtATime: true,
|
|
showProgress: true,
|
|
allowBackNavigation: true
|
|
}
|
|
},
|
|
isDragOver: false,
|
|
selectedFieldId: null,
|
|
currentWizardId: 'wizard-1',
|
|
previewWizardIndex: 0,
|
|
previewProgress: 30,
|
|
showPreview: false,
|
|
showAnalytics: false
|
|
};
|
|
|
|
this.canvasRef = null;
|
|
this.sortableInstance = null;
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.initializeDragAndDrop();
|
|
|
|
// Check for form_id in URL parameters
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const formId = urlParams.get('form_id');
|
|
|
|
if (formId) {
|
|
this.loadFormFromDatabase(formId);
|
|
} else {
|
|
this.loadSampleData();
|
|
}
|
|
|
|
// 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({});
|
|
}
|
|
});
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
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 });
|
|
} else {
|
|
this.setState({ selectedFieldId: null });
|
|
}
|
|
}
|
|
|
|
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({});
|
|
}
|
|
}
|
|
|
|
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({});
|
|
}
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
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 });
|
|
}
|
|
}
|
|
|
|
selectWizard = (wizardId) => {
|
|
this.setState({ currentWizardId: wizardId, selectedFieldId: null });
|
|
}
|
|
|
|
updateFormTitle = (e) => {
|
|
this.setState({ form: { ...this.state.form, title: e.target.value } });
|
|
}
|
|
|
|
updateFormDescription = (e) => {
|
|
this.setState({ form: { ...this.state.form, description: e.target.value } });
|
|
}
|
|
|
|
updateWizardTitle = (e) => {
|
|
const currentWizard = this.getCurrentWizard();
|
|
if (currentWizard) {
|
|
currentWizard.title = e.target.value;
|
|
this.setState({});
|
|
}
|
|
}
|
|
|
|
togglePreview = () => {
|
|
this.setState({
|
|
showPreview: !this.state.showPreview,
|
|
previewWizardIndex: 0
|
|
});
|
|
this.updatePreviewProgress();
|
|
}
|
|
|
|
nextWizard = () => {
|
|
if (this.state.previewWizardIndex < this.state.form.wizards.length - 1) {
|
|
this.setState({ previewWizardIndex: this.state.previewWizardIndex + 1 });
|
|
this.updatePreviewProgress();
|
|
}
|
|
}
|
|
|
|
previousWizard = () => {
|
|
if (this.state.previewWizardIndex > 0) {
|
|
this.setState({ previewWizardIndex: this.state.previewWizardIndex - 1 });
|
|
this.updatePreviewProgress();
|
|
}
|
|
}
|
|
|
|
updatePreviewProgress = () => {
|
|
const progress = ((this.state.previewWizardIndex + 1) / this.state.form.wizards.length) * 100;
|
|
this.setState({ previewProgress: progress });
|
|
}
|
|
|
|
toggleAnalytics = () => {
|
|
this.setState({ showAnalytics: !this.state.showAnalytics });
|
|
}
|
|
|
|
exportForm = () => {
|
|
const formData = JSON.stringify(this.state.form, null, 2);
|
|
const blob = new Blob([formData], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${this.state.form.title || 'form'}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
|
|
saveFormToDatabase = async () => {
|
|
try {
|
|
this.setState({ isSaving: true });
|
|
|
|
const response = await fetch('/api/forms/save/', {
|
|
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,
|
|
form_id: this.state.formId || null
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.setState({
|
|
formId: result.form_id,
|
|
isSaving: false,
|
|
saveMessage: 'Form saved successfully!'
|
|
});
|
|
|
|
// Show success message
|
|
this.showNotification('Form saved successfully!', 'success');
|
|
|
|
// Update URL to include form ID
|
|
if (window.history.pushState) {
|
|
const newUrl = window.location.pathname + '?form_id=' + result.form_id;
|
|
window.history.pushState({form_id: result.form_id}, '', newUrl);
|
|
}
|
|
} else {
|
|
throw new Error(result.error || 'Failed to save form');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving form:', error);
|
|
this.setState({ isSaving: false });
|
|
this.showNotification('Error saving form: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
loadFormFromDatabase = async (formId) => {
|
|
try {
|
|
const response = await fetch(`/api/forms/${formId}/load/`);
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.setState({
|
|
form: result.form.structure,
|
|
formId: result.form.id,
|
|
selectedFieldId: null,
|
|
currentWizardId: result.form.structure.wizards?.[0]?.id || 'wizard-1'
|
|
});
|
|
} else {
|
|
throw new Error(result.error || 'Failed to load form');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading form:', error);
|
|
this.showNotification('Error loading form: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
showNotification = (message, type = 'info') => {
|
|
// Create notification element
|
|
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);
|
|
|
|
// Auto-remove after 5 seconds
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.parentNode.removeChild(notification);
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
getCsrftoken = () => {
|
|
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 '';
|
|
}
|
|
|
|
openPreview = () => {
|
|
if (this.state.formId) {
|
|
window.open(`/forms/${this.state.formId}/`, '_blank');
|
|
} else {
|
|
this.showNotification('Please save the form first to preview it', 'warning');
|
|
}
|
|
}
|
|
|
|
loadSampleData = () => {
|
|
// Add a few sample fields for demonstration
|
|
setTimeout(() => {
|
|
this.addField('text');
|
|
this.addField('email');
|
|
this.addField('file');
|
|
this.addField('dropdown');
|
|
}, 100);
|
|
}
|
|
|
|
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>FormBuilder</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.saveFormToDatabase} disabled=${this.state.isSaving}>
|
|
${this.state.isSaving ? html`<span class="spinner-border spinner-border-sm me-2"></span>Saving...` : html`💾 Save`}
|
|
</button>
|
|
<button class="btn btn-outline" onClick=${this.openPreview}>
|
|
👁️ Preview
|
|
</button>
|
|
<button class="btn btn-primary" onClick=${this.exportForm}>
|
|
📤 Export
|
|
</button>
|
|
<button class="btn btn-outline" onClick=${this.toggleAnalytics}>
|
|
📊 Analytics
|
|
</button>
|
|
</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({}); }} />
|
|
</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({}); }} />
|
|
</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({}); }} />
|
|
<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>
|
|
</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() : ''}
|
|
${this.state.showAnalytics ? this.renderAnalyticsDashboard() : ''}
|
|
`;
|
|
}
|
|
|
|
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" onClick=${this.previousWizard} 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" onClick=${this.nextWizard} disabled=${this.state.previewWizardIndex === this.state.form.wizards.length - 1}>
|
|
${this.state.previewWizardIndex === this.state.form.wizards.length - 1 ? 'Submit' : 'Next'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderAnalyticsDashboard() {
|
|
return html`
|
|
<div class="analytics-dashboard">
|
|
<div class="analytics-header">
|
|
<h2>Form Analytics</h2>
|
|
<button class="btn btn-outline" onClick=${this.toggleAnalytics}>
|
|
✖️ Close Analytics
|
|
</button>
|
|
</div>
|
|
<div class="analytics-content">
|
|
<div class="metric-cards">
|
|
<div class="metric-card">
|
|
<div class="metric-value">1,243</div>
|
|
<div class="metric-label">Total Views</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">342</div>
|
|
<div class="metric-label">Submissions</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">27.5%</div>
|
|
<div class="metric-label">Completion Rate</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value">2:14</div>
|
|
<div class="metric-label">Avg. Time</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-container" id="completion-chart"></div>
|
|
<div class="chart-container" id="response-timeline"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Render the app
|
|
render(html`<${FormBuilder} />`, document.getElementById('app'));
|
|
|
|
// Initialize charts when analytics dashboard is shown
|
|
window.initCharts = function() {
|
|
if (typeof echarts !== 'undefined') {
|
|
// Initialize completion funnel chart
|
|
const completionChart = echarts.init(document.getElementById('completion-chart'));
|
|
const completionOption = {
|
|
title: {
|
|
text: 'Completion Funnel',
|
|
left: 'center'
|
|
},
|
|
tooltip: {
|
|
trigger: 'item',
|
|
formatter: '{a} <br/>{b} : {c}'
|
|
},
|
|
series: [
|
|
{
|
|
name: 'Completion',
|
|
type: 'funnel',
|
|
left: '10%',
|
|
top: 60,
|
|
bottom: 60,
|
|
width: '80%',
|
|
min: 0,
|
|
max: 100,
|
|
minSize: '0%',
|
|
maxSize: '100%',
|
|
sort: 'descending',
|
|
gap: 2,
|
|
label: {
|
|
show: true,
|
|
position: 'inside'
|
|
},
|
|
itemStyle: {
|
|
borderColor: '#fff',
|
|
borderWidth: 1
|
|
},
|
|
data: [
|
|
{ value: 100, name: 'Started' },
|
|
{ value: 80, name: 'In Progress' },
|
|
{ value: 60, name: 'Halfway' },
|
|
{ value: 30, name: 'Almost Done' },
|
|
{ value: 10, name: 'Completed' }
|
|
]
|
|
}
|
|
]
|
|
};
|
|
completionChart.setOption(completionOption);
|
|
|
|
// Initialize response timeline chart
|
|
const timelineChart = echarts.init(document.getElementById('response-timeline'));
|
|
const timelineOption = {
|
|
title: {
|
|
text: 'Response Timeline',
|
|
left: 'center'
|
|
},
|
|
tooltip: {
|
|
trigger: 'axis'
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
|
},
|
|
yAxis: {
|
|
type: 'value'
|
|
},
|
|
series: [
|
|
{
|
|
data: [120, 200, 150, 80, 70, 110, 130],
|
|
type: 'bar',
|
|
itemStyle: {
|
|
color: '#4361ee'
|
|
}
|
|
}
|
|
]
|
|
};
|
|
timelineChart.setOption(timelineOption);
|
|
|
|
// Handle window resize
|
|
window.addEventListener('resize', function() {
|
|
completionChart.resize();
|
|
timelineChart.resize();
|
|
});
|
|
}
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|