Implement live collection preview system with contextual template selection
Replace isolated template previews with live collection reconstruction:
- Frontend now reconstructs collection container with all template variants
- Users click directly on rendered templates in proper CSS context
- Perfect preservation of grid/flex layouts and responsive behavior
- Simplified API: preview endpoint returns container_html + templates for frontend reconstruction
- Enhanced UX: WYSIWYG template selection shows exactly what will be added
- Removed redundant templates endpoint in favor of unified preview approach
Backend changes:
- Add GET /api/collections/{id}/preview endpoint
- Remove GET /api/collections/{id}/templates endpoint
- Return container HTML + templates for frontend reconstruction
Frontend changes:
- Replace isolated template modal with live collection preview
- Add generateLivePreview() method for container reconstruction
- Update CollectionManager to use preview API
- Add interactive CSS styling for template selection
This provides true contextual template selection where CSS inheritance,
grid layouts, and responsive design work perfectly in preview mode.
This commit is contained in:
@@ -243,25 +243,25 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available templates for a collection
|
||||
* Get collection preview data (container + templates for frontend reconstruction)
|
||||
* @param {string} collectionId - Collection ID
|
||||
* @returns {Promise<Array>} Array of collection templates
|
||||
* @returns {Promise<Object>} Object with collection_id, container_html, and templates
|
||||
*/
|
||||
async getCollectionTemplates(collectionId) {
|
||||
async getCollectionPreview(collectionId) {
|
||||
try {
|
||||
const collectionsUrl = this.getCollectionsUrl();
|
||||
const response = await fetch(`${collectionsUrl}/${collectionId}/templates?site_id=${this.siteId}`);
|
||||
const response = await fetch(`${collectionsUrl}/${collectionId}/preview?site_id=${this.siteId}`);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
return result.templates || [];
|
||||
return result;
|
||||
} else {
|
||||
console.warn(`⚠️ Failed to fetch collection templates (${response.status}): ${collectionId}`);
|
||||
return [];
|
||||
console.warn(`⚠️ Failed to fetch collection preview (${response.status}): ${collectionId}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch collection templates:', collectionId, error);
|
||||
return [];
|
||||
console.error('Failed to fetch collection preview:', collectionId, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1098,3 +1098,147 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
LIVE COLLECTION PREVIEW MODAL
|
||||
================================================================= */
|
||||
|
||||
.insertr-collection-preview-modal {
|
||||
background: var(--insertr-bg-primary);
|
||||
color: var(--insertr-text-primary);
|
||||
border-radius: var(--insertr-border-radius);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: var(--insertr-z-modal);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.insertr-preview-header {
|
||||
padding: var(--insertr-spacing-lg);
|
||||
border-bottom: 1px solid var(--insertr-border-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.insertr-preview-header h3 {
|
||||
margin: 0 0 var(--insertr-spacing-xs) 0;
|
||||
font-size: 1.2rem;
|
||||
color: var(--insertr-text-primary);
|
||||
}
|
||||
|
||||
.insertr-preview-header p {
|
||||
margin: 0;
|
||||
color: var(--insertr-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.insertr-preview-container {
|
||||
padding: var(--insertr-spacing-lg);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.insertr-preview-actions {
|
||||
padding: var(--insertr-spacing-md) var(--insertr-spacing-lg);
|
||||
border-top: 1px solid var(--insertr-border-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--insertr-spacing-md);
|
||||
}
|
||||
|
||||
/* Preview item selection styling */
|
||||
.insertr-preview-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
border-radius: var(--insertr-border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.insertr-preview-item:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.25);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.insertr-preview-item::after {
|
||||
content: 'Click to select';
|
||||
position: absolute;
|
||||
top: var(--insertr-spacing-xs);
|
||||
right: var(--insertr-spacing-xs);
|
||||
background: rgba(59, 130, 246, 0.95);
|
||||
color: white;
|
||||
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
|
||||
border-radius: var(--insertr-border-radius);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.insertr-preview-item:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Enhanced hover effect for better visual feedback */
|
||||
.insertr-preview-item:hover {
|
||||
outline: 2px solid var(--insertr-primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Button styling for preview modal */
|
||||
.insertr-template-btn {
|
||||
background: var(--insertr-bg-secondary);
|
||||
color: var(--insertr-text-primary);
|
||||
border: 1px solid var(--insertr-border-color);
|
||||
border-radius: var(--insertr-border-radius);
|
||||
padding: var(--insertr-spacing-sm) var(--insertr-spacing-md);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.insertr-template-btn:hover {
|
||||
background: var(--insertr-bg-hover);
|
||||
border-color: var(--insertr-primary-color);
|
||||
}
|
||||
|
||||
.insertr-template-btn-cancel {
|
||||
background: var(--insertr-bg-secondary);
|
||||
color: var(--insertr-text-primary);
|
||||
}
|
||||
|
||||
.insertr-template-btn-cancel:hover {
|
||||
background: var(--insertr-danger-color);
|
||||
color: white;
|
||||
border-color: var(--insertr-danger-color);
|
||||
}
|
||||
|
||||
/* Mobile responsiveness for preview modal */
|
||||
@media (max-width: 768px) {
|
||||
.insertr-collection-preview-modal {
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
margin: var(--insertr-spacing-sm);
|
||||
}
|
||||
|
||||
.insertr-preview-container {
|
||||
padding: var(--insertr-spacing-md);
|
||||
}
|
||||
|
||||
.insertr-preview-item:hover {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.insertr-preview-item::after {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class CollectionManager {
|
||||
this.template = null;
|
||||
this.items = [];
|
||||
this.isActive = false;
|
||||
this.cachedTemplates = null; // Cache for available templates
|
||||
this.cachedPreview = null; // Cache for collection preview data
|
||||
|
||||
// UI elements
|
||||
this.addButton = null;
|
||||
@@ -412,16 +412,16 @@ export class CollectionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Get available templates for this collection
|
||||
const templates = await this.getAvailableTemplates();
|
||||
if (!templates || templates.length === 0) {
|
||||
// 1. Get collection preview data
|
||||
const previewData = await this.getCollectionPreview();
|
||||
if (!previewData || !previewData.templates || previewData.templates.length === 0) {
|
||||
console.error('❌ No templates available for collection:', this.collectionId);
|
||||
alert('No templates available for this collection. Please refresh the page.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Select template (auto-select if only one, otherwise present options)
|
||||
const selectedTemplate = await this.selectTemplate(templates);
|
||||
// 2. Select template (auto-select if only one, otherwise show live preview)
|
||||
const selectedTemplate = await this.selectTemplate(previewData);
|
||||
if (!selectedTemplate) {
|
||||
console.log('Template selection cancelled by user');
|
||||
return;
|
||||
@@ -465,46 +465,48 @@ export class CollectionManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available templates for this collection
|
||||
* @returns {Promise<Array>} Array of template objects
|
||||
* Get collection preview data (container + templates)
|
||||
* @returns {Promise<Object>} Object with collection_id, container_html, and templates
|
||||
*/
|
||||
async getAvailableTemplates() {
|
||||
async getCollectionPreview() {
|
||||
try {
|
||||
if (!this.cachedTemplates) {
|
||||
console.log('🔍 Fetching templates for collection:', this.collectionId);
|
||||
this.cachedTemplates = await this.apiClient.getCollectionTemplates(this.collectionId);
|
||||
console.log('📋 Templates fetched:', this.cachedTemplates);
|
||||
if (!this.cachedPreview) {
|
||||
console.log('🔍 Fetching preview for collection:', this.collectionId);
|
||||
this.cachedPreview = await this.apiClient.getCollectionPreview(this.collectionId);
|
||||
console.log('📋 Preview fetched:', this.cachedPreview);
|
||||
}
|
||||
return this.cachedTemplates;
|
||||
return this.cachedPreview;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch templates for collection:', this.collectionId, error);
|
||||
return [];
|
||||
console.error('❌ Failed to fetch preview for collection:', this.collectionId, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a template for creating new items
|
||||
* @param {Array} templates - Available templates
|
||||
* @param {Object} previewData - Preview data with container_html and templates
|
||||
* @returns {Promise<Object|null>} Selected template or null if cancelled
|
||||
*/
|
||||
async selectTemplate(templates) {
|
||||
async selectTemplate(previewData) {
|
||||
const templates = previewData.templates;
|
||||
|
||||
// Auto-select if only one template
|
||||
if (templates.length === 1) {
|
||||
console.log('🎯 Auto-selecting single template:', templates[0].name);
|
||||
return templates[0];
|
||||
}
|
||||
|
||||
// Present selection UI for multiple templates
|
||||
console.log('🎨 Multiple templates available, showing selection UI');
|
||||
return this.showTemplateSelectionModal(templates);
|
||||
// Present live collection preview for multiple templates
|
||||
console.log('🎨 Multiple templates available, showing live preview');
|
||||
return this.showLiveCollectionPreview(previewData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show template selection modal
|
||||
* @param {Array} templates - Available templates
|
||||
* Show live collection preview for template selection
|
||||
* @param {Object} previewData - Preview data with container_html and templates
|
||||
* @returns {Promise<Object|null>} Selected template or null if cancelled
|
||||
*/
|
||||
async showTemplateSelectionModal(templates) {
|
||||
async showLiveCollectionPreview(previewData) {
|
||||
return new Promise((resolve) => {
|
||||
// Create modal overlay
|
||||
const overlay = document.createElement('div');
|
||||
@@ -512,73 +514,38 @@ export class CollectionManager {
|
||||
|
||||
// Create modal content
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'insertr-template-selector';
|
||||
modal.className = 'insertr-collection-preview-modal';
|
||||
|
||||
// Generate live preview by reconstructing collection with all templates
|
||||
const previewHTML = this.generateLivePreview(previewData.container_html, previewData.templates);
|
||||
|
||||
// Create modal HTML using CSS classes
|
||||
modal.innerHTML = `
|
||||
<h3>Choose Template</h3>
|
||||
<div class="insertr-template-options">
|
||||
${templates.map(template => `
|
||||
<div class="insertr-template-option ${template.is_default ? 'insertr-template-default' : ''}"
|
||||
data-template-id="${template.template_id}">
|
||||
<div class="insertr-template-name">
|
||||
${template.name}${template.is_default ? ' (default)' : ''}
|
||||
</div>
|
||||
<div class="insertr-template-preview-container">
|
||||
${this.createStyledTemplatePreview(template.html_template)}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
<div class="insertr-preview-header">
|
||||
<h3>Choose Template</h3>
|
||||
<p>Click on the item you want to add</p>
|
||||
</div>
|
||||
<div class="insertr-template-actions">
|
||||
<div class="insertr-preview-container">
|
||||
${previewHTML}
|
||||
</div>
|
||||
<div class="insertr-preview-actions">
|
||||
<button class="insertr-template-btn insertr-template-btn-cancel">Cancel</button>
|
||||
<button class="insertr-template-btn insertr-template-btn-select" disabled>Add Item</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let selectedTemplate = null;
|
||||
let userHasInteracted = false;
|
||||
|
||||
// Add event listeners for template selection
|
||||
modal.querySelectorAll('.insertr-template-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => {
|
||||
// Mark that user has interacted
|
||||
userHasInteracted = true;
|
||||
// Handle template selection by clicking on preview items
|
||||
modal.addEventListener('click', (e) => {
|
||||
const previewItem = e.target.closest('.insertr-preview-item');
|
||||
if (previewItem) {
|
||||
const templateId = parseInt(previewItem.dataset.templateId);
|
||||
const selectedTemplate = previewData.templates.find(t => t.template_id === templateId);
|
||||
|
||||
// Remove previous selection from all options
|
||||
modal.querySelectorAll('.insertr-template-option').forEach(opt => {
|
||||
opt.classList.remove('insertr-template-selected');
|
||||
});
|
||||
|
||||
// Add selection class to clicked option
|
||||
option.classList.add('insertr-template-selected');
|
||||
|
||||
// Find selected template
|
||||
const templateId = parseInt(option.dataset.templateId);
|
||||
selectedTemplate = templates.find(t => t.template_id === templateId);
|
||||
|
||||
// Enable select button
|
||||
const selectBtn = modal.querySelector('.insertr-template-btn-select');
|
||||
selectBtn.disabled = false;
|
||||
|
||||
console.log('🎯 Template selected by user:', selectedTemplate.name);
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-select default template only if user hasn't interacted
|
||||
const defaultTemplate = templates.find(t => t.is_default);
|
||||
if (defaultTemplate) {
|
||||
// Set initial selection without triggering click event
|
||||
selectedTemplate = defaultTemplate;
|
||||
const defaultOption = modal.querySelector(`[data-template-id="${defaultTemplate.template_id}"]`);
|
||||
if (defaultOption) {
|
||||
// Apply visual selection without click event
|
||||
defaultOption.classList.add('insertr-template-selected');
|
||||
const selectBtn = modal.querySelector('.insertr-template-btn-select');
|
||||
selectBtn.disabled = false;
|
||||
console.log('🎯 Default template pre-selected:', defaultTemplate.name);
|
||||
if (selectedTemplate) {
|
||||
console.log('🎯 Template selected from preview:', selectedTemplate.name);
|
||||
document.body.removeChild(overlay);
|
||||
resolve(selectedTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel button handler
|
||||
modal.querySelector('.insertr-template-btn-cancel').addEventListener('click', () => {
|
||||
@@ -586,13 +553,6 @@ export class CollectionManager {
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
// Select button handler
|
||||
modal.querySelector('.insertr-template-btn-select').addEventListener('click', () => {
|
||||
console.log('✅ Adding item with template:', selectedTemplate ? selectedTemplate.name : 'none');
|
||||
document.body.removeChild(overlay);
|
||||
resolve(selectedTemplate);
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
@@ -983,4 +943,57 @@ export class CollectionManager {
|
||||
|
||||
console.log('🧹 CollectionManager destroyed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate live collection preview by reconstructing container with all template variants
|
||||
* @param {string} containerHTML - Collection container HTML
|
||||
* @param {Array} templates - Array of template objects
|
||||
* @returns {string} HTML string with reconstructed collection
|
||||
*/
|
||||
generateLivePreview(containerHTML, templates) {
|
||||
try {
|
||||
// Parse the container HTML
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = containerHTML;
|
||||
const collectionContainer = tempContainer.querySelector('.insertr-add');
|
||||
|
||||
if (!collectionContainer) {
|
||||
console.error('❌ No .insertr-add container found in collection HTML');
|
||||
return '<div>Preview generation failed</div>';
|
||||
}
|
||||
|
||||
// Clear existing children
|
||||
collectionContainer.innerHTML = '';
|
||||
|
||||
// Add one instance of each template with preview classes
|
||||
templates.forEach(template => {
|
||||
// Parse template HTML
|
||||
const templateContainer = document.createElement('div');
|
||||
templateContainer.innerHTML = template.html_template;
|
||||
const templateElement = templateContainer.firstElementChild;
|
||||
|
||||
if (templateElement) {
|
||||
// Clone the template element
|
||||
const previewElement = templateElement.cloneNode(true);
|
||||
|
||||
// Add preview classes and data attributes for selection
|
||||
previewElement.classList.add('insertr-preview-item');
|
||||
previewElement.setAttribute('data-template-id', template.template_id);
|
||||
previewElement.setAttribute('data-template-name', template.name);
|
||||
|
||||
// Add the preview element to the collection container
|
||||
collectionContainer.appendChild(previewElement);
|
||||
|
||||
console.log(`✅ Added template ${template.template_id} (${template.name}) to preview`);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the complete collection HTML
|
||||
return tempContainer.innerHTML;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to generate live preview:', error);
|
||||
return '<div>Preview generation failed</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user