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:
2025-10-31 22:41:12 +01:00
parent 81ec8edf36
commit 163cbf7eea
4 changed files with 273 additions and 106 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>';
}
}
}