@visns-studio/visns-components 5.11.4 → 5.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,412 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, Save, Tag, Settings, Eye } from 'lucide-react';
3
+ import Field from '../crm/Field';
4
+ import VariableInserter from './VariableInserter';
5
+ import './SectionEditor.css';
6
+
7
+ const SectionEditor = ({ section, availableVariables = [], onSave, onCancel }) => {
8
+ const [formData, setFormData] = useState({
9
+ id: section?.id || null,
10
+ title: section?.title || '',
11
+ section_type: section?.section_type || '',
12
+ content: section?.content || '',
13
+ variables: section?.variables || [],
14
+ styling: section?.styling || {},
15
+ is_enabled: section?.is_enabled !== false,
16
+ is_dynamic: section?.is_dynamic !== false,
17
+ sort_order: section?.sort_order || 1
18
+ });
19
+
20
+ const [showVariableInserter, setShowVariableInserter] = useState(false);
21
+ const [showStylingOptions, setShowStylingOptions] = useState(false);
22
+ const [isPreview, setIsPreview] = useState(false);
23
+
24
+ useEffect(() => {
25
+ if (section) {
26
+ setFormData({
27
+ id: section.id || null,
28
+ title: section.title || '',
29
+ section_type: section.section_type || '',
30
+ content: section.content || '',
31
+ variables: section.variables || [],
32
+ styling: section.styling || {},
33
+ is_enabled: section.is_enabled !== false,
34
+ is_dynamic: section.is_dynamic !== false,
35
+ sort_order: section.sort_order || 1
36
+ });
37
+ }
38
+ }, [section]);
39
+
40
+ const handleInputChange = (field, value) => {
41
+ console.log(`Field change: ${field} =`, value);
42
+ setFormData(prev => ({
43
+ ...prev,
44
+ [field]: value
45
+ }));
46
+ };
47
+
48
+ const handleStylingChange = (property, value) => {
49
+ setFormData(prev => ({
50
+ ...prev,
51
+ styling: {
52
+ ...prev.styling,
53
+ [property]: value
54
+ }
55
+ }));
56
+ };
57
+
58
+ const handleVariableInsert = (variable) => {
59
+ const variableTag = `{{${variable}}}`;
60
+
61
+ // Try to insert at cursor position in TinyMCE editor
62
+ if (window.tinymce && window.tinymce.activeEditor) {
63
+ window.tinymce.activeEditor.insertContent(variableTag);
64
+ } else {
65
+ // Fallback: append to content
66
+ handleInputChange('content', formData.content + variableTag);
67
+ }
68
+
69
+ // Add to variables array if not already present
70
+ if (!formData.variables.includes(variable)) {
71
+ setFormData(prev => ({
72
+ ...prev,
73
+ variables: [...prev.variables, variable]
74
+ }));
75
+ }
76
+
77
+ setShowVariableInserter(false);
78
+ };
79
+
80
+ const handleSave = () => {
81
+ if (!formData.title.trim()) {
82
+ alert('Please enter a section title');
83
+ return;
84
+ }
85
+
86
+ // Try to get the latest content from TinyMCE editor before saving
87
+ let currentContent = formData.content;
88
+
89
+ // Multiple approaches to get TinyMCE content
90
+ if (window.tinymce) {
91
+ // Try active editor first
92
+ if (window.tinymce.activeEditor) {
93
+ currentContent = window.tinymce.activeEditor.getContent();
94
+ console.log('TinyMCE content from activeEditor:', currentContent);
95
+ }
96
+ // Try to find editor by content field
97
+ else if (window.tinymce.get('content')) {
98
+ currentContent = window.tinymce.get('content').getContent();
99
+ console.log('TinyMCE content from content field:', currentContent);
100
+ }
101
+ // Try to find any editor instances
102
+ else if (window.tinymce.editors && window.tinymce.editors.length > 0) {
103
+ currentContent = window.tinymce.editors[0].getContent();
104
+ console.log('TinyMCE content from first editor:', currentContent);
105
+ }
106
+ }
107
+
108
+ const finalFormData = {
109
+ ...formData,
110
+ content: currentContent
111
+ };
112
+
113
+ console.log('Final form data being saved:', finalFormData);
114
+ onSave(finalFormData);
115
+ };
116
+
117
+ const renderPreview = () => {
118
+ // Ensure content is a string
119
+ let previewContent = formData.content || '';
120
+
121
+ // Convert to string if it's not already
122
+ if (typeof previewContent !== 'string') {
123
+ previewContent = String(previewContent);
124
+ }
125
+
126
+ // If content is empty, show placeholder
127
+ if (!previewContent.trim()) {
128
+ return (
129
+ <div className="prose prose-sm max-w-none border border-gray-200 rounded-lg p-4 bg-gray-50">
130
+ <div className="text-gray-500 italic">No content to preview. Add some content to see the preview.</div>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ // Replace variables with sample data for preview
136
+ const sampleData = {
137
+ customer_name: 'ACME Corporation',
138
+ company_name: 'OMNIA Global Group',
139
+ project_name: 'Digital Transformation Project',
140
+ quote_total: '$25,000.00',
141
+ quote_date: new Date().toLocaleDateString(),
142
+ salesperson_name: 'John Smith'
143
+ };
144
+
145
+ try {
146
+ Object.entries(sampleData).forEach(([key, value]) => {
147
+ const regex = new RegExp(`{{${key}}}`, 'g');
148
+ previewContent = previewContent.replace(regex, `<span class="bg-yellow-100 px-1 font-medium">${value}</span>`);
149
+ });
150
+ } catch (error) {
151
+ console.error('Error processing preview content:', error);
152
+ previewContent = formData.content || '';
153
+ }
154
+
155
+ return (
156
+ <div className="prose prose-sm max-w-none border border-gray-200 rounded-lg p-4 bg-gray-50">
157
+ <div dangerouslySetInnerHTML={{ __html: previewContent }} />
158
+ </div>
159
+ );
160
+ };
161
+
162
+ return (
163
+ <div className="section-editor-modal">
164
+ <div className="section-editor-content">
165
+ <div className="section-editor-header">
166
+ <div>
167
+ <h2 className="section-editor-title">
168
+ {section?.id ? 'Edit Section' : 'New Section'}
169
+ </h2>
170
+ <p className="section-editor-subtitle">
171
+ Section Type: {formData.section_type?.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
172
+ </p>
173
+ </div>
174
+ <div className="section-editor-header-actions">
175
+ <button
176
+ onClick={() => setIsPreview(!isPreview)}
177
+ className={`section-editor-preview-toggle ${
178
+ isPreview ? 'active' : 'inactive'
179
+ }`}
180
+ >
181
+ <Eye size={16} />
182
+ {isPreview ? 'Edit' : 'Preview'}
183
+ </button>
184
+ <button
185
+ onClick={onCancel}
186
+ className="section-editor-close-btn"
187
+ >
188
+ <X size={20} />
189
+ </button>
190
+ </div>
191
+ </div>
192
+
193
+ <div className="section-editor-body">
194
+ {/* Basic Section Info */}
195
+ <div className="section-editor-form-grid">
196
+ <div className="section-editor-form-group">
197
+ <Field
198
+ settings={{
199
+ id: "title",
200
+ label: "Section Title",
201
+ type: "text",
202
+ value: formData.title,
203
+ required: true,
204
+ placeholder: "Enter section title"
205
+ }}
206
+ inputClass={{ title: "form-control" }}
207
+ inputValue={formData.title}
208
+ onChange={(event) => {
209
+ const value = event?.target?.value || event;
210
+ handleInputChange('title', value);
211
+ }}
212
+ />
213
+ </div>
214
+ <div className="section-editor-checkbox-group">
215
+ <div className="section-editor-checkbox">
216
+ <input
217
+ type="checkbox"
218
+ id="is_enabled"
219
+ checked={formData.is_enabled}
220
+ onChange={(e) => handleInputChange('is_enabled', e.target.checked)}
221
+ />
222
+ <label htmlFor="is_enabled">Enabled</label>
223
+ </div>
224
+ <div className="section-editor-checkbox">
225
+ <input
226
+ type="checkbox"
227
+ id="is_dynamic"
228
+ checked={formData.is_dynamic}
229
+ onChange={(e) => handleInputChange('is_dynamic', e.target.checked)}
230
+ />
231
+ <label htmlFor="is_dynamic">Dynamic Content</label>
232
+ </div>
233
+ </div>
234
+ </div>
235
+
236
+ {/* Content Editor or Preview - Only show if not dynamic */}
237
+ {!formData.is_dynamic ? (
238
+ <div className="section-editor-content-section">
239
+ <div className="section-editor-content-header">
240
+ <label className="section-editor-content-label">
241
+ Section Content
242
+ </label>
243
+ <div className="section-editor-content-actions">
244
+ <button
245
+ onClick={() => setShowVariableInserter(true)}
246
+ className="section-editor-action-btn primary"
247
+ >
248
+ <Tag size={14} />
249
+ Insert Variable
250
+ </button>
251
+ <button
252
+ onClick={() => setShowStylingOptions(!showStylingOptions)}
253
+ className="section-editor-action-btn"
254
+ >
255
+ <Settings size={14} />
256
+ Styling
257
+ </button>
258
+ </div>
259
+ </div>
260
+
261
+ {isPreview ? (
262
+ <div className="section-editor-preview">
263
+ {renderPreview()}
264
+ </div>
265
+ ) : (
266
+ <Field
267
+ settings={{
268
+ id: "content",
269
+ type: "richeditor",
270
+ value: formData.content,
271
+ rows: 15,
272
+ placeholder: "Enter section content. Use {{variable_name}} for dynamic content.",
273
+ editorConfig: {
274
+ height: 400,
275
+ plugins: [
276
+ 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
277
+ 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
278
+ 'insertdatetime', 'media', 'table', 'help', 'wordcount'
279
+ ],
280
+ toolbar: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | help',
281
+ content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px }'
282
+ }
283
+ }}
284
+ inputClass={{ content: "form-control" }}
285
+ inputValue={formData.content}
286
+ onChangeRicheditor={(value) => handleInputChange('content', value)}
287
+ onChange={(event) => {
288
+ const value = event?.target?.value || event;
289
+ handleInputChange('content', value);
290
+ }}
291
+ onBlur={() => {
292
+ // Force content sync when editor loses focus
293
+ if (window.tinymce && window.tinymce.activeEditor) {
294
+ const content = window.tinymce.activeEditor.getContent();
295
+ if (content !== formData.content) {
296
+ handleInputChange('content', content);
297
+ }
298
+ }
299
+ }}
300
+ />
301
+ )}
302
+ </div>
303
+ ) : (
304
+ /* Dynamic Content Notice */
305
+ <div className="section-editor-dynamic-notice">
306
+ <div className="section-editor-dynamic-card">
307
+ <div className="section-editor-dynamic-icon">
308
+ <Tag size={20} />
309
+ </div>
310
+ <div className="section-editor-dynamic-content">
311
+ <h4 className="section-editor-dynamic-title">Dynamic Content Enabled</h4>
312
+ <p className="section-editor-dynamic-description">
313
+ This section will be automatically generated using dynamic variables and templates.
314
+ Content will be populated when the proposal is generated based on the selected data sources.
315
+ </p>
316
+ <div className="section-editor-dynamic-actions">
317
+ <button
318
+ onClick={() => setShowStylingOptions(!showStylingOptions)}
319
+ className="section-editor-action-btn"
320
+ >
321
+ <Settings size={14} />
322
+ Styling Options
323
+ </button>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ )}
329
+
330
+ {/* Styling Options */}
331
+ {showStylingOptions && (
332
+ <div className="section-editor-styling-panel">
333
+ <h4 className="section-editor-styling-title">Section Styling</h4>
334
+ <div className="section-editor-styling-grid">
335
+ <div className="section-editor-styling-option">
336
+ <input
337
+ type="checkbox"
338
+ id="page_break_before"
339
+ checked={formData.styling.page_break_before || false}
340
+ onChange={(e) => handleStylingChange('page_break_before', e.target.checked)}
341
+ />
342
+ <label htmlFor="page_break_before">Page break before</label>
343
+ </div>
344
+ <div className="section-editor-styling-option">
345
+ <input
346
+ type="checkbox"
347
+ id="page_break_after"
348
+ checked={formData.styling.page_break_after || false}
349
+ onChange={(e) => handleStylingChange('page_break_after', e.target.checked)}
350
+ />
351
+ <label htmlFor="page_break_after">Page break after</label>
352
+ </div>
353
+ <div>
354
+ <label className="section-editor-form-label">Margin Top (px)</label>
355
+ <input
356
+ type="number"
357
+ value={formData.styling.margin_top || ''}
358
+ onChange={(e) => handleStylingChange('margin_top', e.target.value)}
359
+ placeholder="0"
360
+ className="section-editor-styling-input"
361
+ />
362
+ </div>
363
+ </div>
364
+ </div>
365
+ )}
366
+
367
+ {/* Variables Summary */}
368
+ {formData.variables.length > 0 && (
369
+ <div className="section-editor-variables-summary">
370
+ <h4 className="section-editor-variables-title">Variables Used in This Section</h4>
371
+ <div className="section-editor-variables-list">
372
+ {formData.variables.map((variable, index) => (
373
+ <span key={index} className="section-editor-variable-tag">
374
+ {`{{${variable}}}`}
375
+ </span>
376
+ ))}
377
+ </div>
378
+ </div>
379
+ )}
380
+ </div>
381
+
382
+ {/* Footer */}
383
+ <div className="section-editor-footer">
384
+ <button
385
+ onClick={onCancel}
386
+ className="section-editor-btn secondary"
387
+ >
388
+ Cancel
389
+ </button>
390
+ <button
391
+ onClick={handleSave}
392
+ className="section-editor-btn primary"
393
+ >
394
+ <Save size={16} />
395
+ Save Section
396
+ </button>
397
+ </div>
398
+
399
+ {/* Variable Inserter Modal */}
400
+ {showVariableInserter && (
401
+ <VariableInserter
402
+ availableVariables={availableVariables}
403
+ onInsert={handleVariableInsert}
404
+ onCancel={() => setShowVariableInserter(false)}
405
+ />
406
+ )}
407
+ </div>
408
+ </div>
409
+ );
410
+ };
411
+
412
+ export default SectionEditor;
@@ -0,0 +1,277 @@
1
+ /* Section Type Selector Modal Styles */
2
+ .section-type-modal {
3
+ position: fixed;
4
+ inset: 0;
5
+ background-color: rgba(0, 0, 0, 0.4);
6
+ backdrop-filter: blur(4px);
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ z-index: 50;
11
+ animation: fadeIn 0.2s ease-out;
12
+ }
13
+
14
+ @keyframes fadeIn {
15
+ from { opacity: 0; }
16
+ to { opacity: 1; }
17
+ }
18
+
19
+ .section-type-modal-content {
20
+ background: white;
21
+ border-radius: 8px;
22
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
23
+ max-width: 900px;
24
+ width: 100%;
25
+ margin: 1rem;
26
+ max-height: 90vh;
27
+ overflow-y: auto;
28
+ animation: slideUp 0.3s ease-out;
29
+ }
30
+
31
+ @keyframes slideUp {
32
+ from {
33
+ opacity: 0;
34
+ transform: translateY(20px) scale(0.95);
35
+ }
36
+ to {
37
+ opacity: 1;
38
+ transform: translateY(0) scale(1);
39
+ }
40
+ }
41
+
42
+ .section-type-modal-header {
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: space-between;
46
+ padding: 1.5rem;
47
+ border-bottom: 1px solid #e2e8f0;
48
+ background: #1b3933;
49
+ color: white;
50
+ border-radius: 8px 8px 0 0;
51
+ }
52
+
53
+ .section-type-modal-header .section-type-modal-title {
54
+ font-size: 1.25rem;
55
+ font-weight: 700;
56
+ margin: 0;
57
+ color: white !important;
58
+ }
59
+
60
+ .section-type-modal-subtitle {
61
+ font-size: 0.9rem;
62
+ margin: 0.25rem 0 0 0;
63
+ opacity: 1;
64
+ color: rgba(255, 255, 255, 0.9);
65
+ }
66
+
67
+ .section-type-close-btn {
68
+ padding: 0.5rem;
69
+ color: white;
70
+ background: rgba(255, 255, 255, 0.1);
71
+ border: none;
72
+ border-radius: 5px;
73
+ cursor: pointer;
74
+ transition: background-color 0.2s;
75
+ }
76
+
77
+ .section-type-close-btn:hover {
78
+ background: #3cbf7d;
79
+ }
80
+
81
+ .section-type-modal-body {
82
+ padding: 1.5rem;
83
+ }
84
+
85
+ .section-category {
86
+ margin-bottom: 2rem;
87
+ }
88
+
89
+ .section-category:last-child {
90
+ margin-bottom: 0;
91
+ }
92
+
93
+ .section-category-title {
94
+ font-size: 1.1rem;
95
+ font-weight: 600;
96
+ color: #1e293b;
97
+ margin-bottom: 1rem;
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.5rem;
101
+ }
102
+
103
+ .section-types-grid {
104
+ display: grid;
105
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
106
+ gap: 1rem;
107
+ }
108
+
109
+ .section-type-card {
110
+ text-align: left;
111
+ padding: 1rem;
112
+ border: 2px solid #e2e8f0;
113
+ border-radius: 8px;
114
+ cursor: pointer;
115
+ transition: all 0.2s ease-in-out;
116
+ background: white;
117
+ position: relative;
118
+ overflow: hidden;
119
+ }
120
+
121
+ .section-type-card::before {
122
+ content: '';
123
+ position: absolute;
124
+ top: 0;
125
+ left: 0;
126
+ right: 0;
127
+ height: 3px;
128
+ background: transparent;
129
+ transition: background 0.2s ease-in-out;
130
+ }
131
+
132
+ .section-type-card:hover {
133
+ border-color: #3b82f6;
134
+ box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
135
+ transform: translateY(-2px);
136
+ }
137
+
138
+ .section-type-card:hover::before {
139
+ background: #3cbf7d;
140
+ }
141
+
142
+ .section-type-card.header-category {
143
+ border-color: #e0f2fe;
144
+ background: #f8fafc;
145
+ }
146
+
147
+ .section-type-card.header-category:hover {
148
+ border-color: #1b3933;
149
+ box-shadow: 0 8px 25px rgba(27, 57, 51, 0.15);
150
+ }
151
+
152
+ .section-type-card.content-category {
153
+ border-color: #ecfdf5;
154
+ background: #f8fafc;
155
+ }
156
+
157
+ .section-type-card.content-category:hover {
158
+ border-color: #3cbf7d;
159
+ box-shadow: 0 8px 25px rgba(60, 191, 125, 0.15);
160
+ }
161
+
162
+ .section-type-card.pricing-category {
163
+ border-color: #fef3c7;
164
+ background: #f8fafc;
165
+ }
166
+
167
+ .section-type-card.pricing-category:hover {
168
+ border-color: #1b3933;
169
+ box-shadow: 0 8px 25px rgba(27, 57, 51, 0.15);
170
+ }
171
+
172
+ .section-type-card.footer-category {
173
+ border-color: #f3e8ff;
174
+ background: #f8fafc;
175
+ }
176
+
177
+ .section-type-card.footer-category:hover {
178
+ border-color: #1b3933;
179
+ box-shadow: 0 8px 25px rgba(27, 57, 51, 0.15);
180
+ }
181
+
182
+ .section-type-card-header {
183
+ display: flex;
184
+ align-items: flex-start;
185
+ gap: 0.75rem;
186
+ margin-bottom: 0.75rem;
187
+ }
188
+
189
+ .section-type-icon {
190
+ flex-shrink: 0;
191
+ padding: 0.5rem;
192
+ border-radius: 5px;
193
+ background: rgba(60, 191, 125, 0.1);
194
+ color: #1b3933;
195
+ }
196
+
197
+ .section-type-card-content {
198
+ flex: 1;
199
+ min-width: 0;
200
+ }
201
+
202
+ .section-type-name {
203
+ font-size: 1rem;
204
+ font-weight: 600;
205
+ color: #1e293b;
206
+ margin: 0 0 0.25rem 0;
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 0.5rem;
210
+ }
211
+
212
+ .section-type-description {
213
+ font-size: 0.875rem;
214
+ color: #64748b;
215
+ margin: 0;
216
+ line-height: 1.4;
217
+ }
218
+
219
+ .dynamic-badge {
220
+ padding: 0.15rem 0.4rem;
221
+ font-size: 0.65rem;
222
+ font-weight: 600;
223
+ background-color: #dcfce7;
224
+ color: #1b3933;
225
+ border-radius: 4px;
226
+ text-transform: uppercase;
227
+ letter-spacing: 0.025em;
228
+ }
229
+
230
+ .section-type-modal-footer {
231
+ display: flex;
232
+ align-items: center;
233
+ justify-content: flex-end;
234
+ padding: 1rem 1.5rem;
235
+ border-top: 1px solid #e2e8f0;
236
+ background: #f8fafc;
237
+ border-radius: 0 0 8px 8px;
238
+ }
239
+
240
+ .section-type-cancel-btn {
241
+ padding: 0.5rem 1rem;
242
+ font-size: 0.875rem;
243
+ font-weight: 500;
244
+ color: #64748b;
245
+ background: white;
246
+ border: 1px solid #e1e1e1;
247
+ border-radius: 5px;
248
+ cursor: pointer;
249
+ transition: all 0.2s;
250
+ }
251
+
252
+ .section-type-cancel-btn:hover {
253
+ background: #f8fafc;
254
+ border-color: #9ca3af;
255
+ }
256
+
257
+ /* Responsive adjustments */
258
+ @media (max-width: 768px) {
259
+ .section-type-modal-content {
260
+ max-width: none;
261
+ margin: 0.5rem;
262
+ border-radius: 5px;
263
+ }
264
+
265
+ .section-types-grid {
266
+ grid-template-columns: 1fr;
267
+ }
268
+
269
+ .section-type-modal-header {
270
+ padding: 1rem;
271
+ border-radius: 5px 5px 0 0;
272
+ }
273
+
274
+ .section-type-modal-body {
275
+ padding: 1rem;
276
+ }
277
+ }