From 163cbf7eea73673a98899bca7d98edff6f38bb69 Mon Sep 17 00:00:00 2001 From: Joakim Date: Fri, 31 Oct 2025 22:41:12 +0100 Subject: [PATCH] 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. --- internal/api/handlers.go | 26 +++-- lib/src/core/api-client.js | 18 +-- lib/src/styles/insertr.css | 144 +++++++++++++++++++++++ lib/src/ui/collection-manager.js | 191 +++++++++++++++++-------------- 4 files changed, 273 insertions(+), 106 deletions(-) diff --git a/internal/api/handlers.go b/internal/api/handlers.go index ee5a09b..a3611dd 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -405,8 +405,8 @@ func (h *ContentHandler) GetCollectionItems(w http.ResponseWriter, r *http.Reque json.NewEncoder(w).Encode(items) } -// GetCollectionTemplates handles GET /api/collections/{id}/templates -func (h *ContentHandler) GetCollectionTemplates(w http.ResponseWriter, r *http.Request) { +// GetCollectionPreview handles GET /api/collections/{id}/preview +func (h *ContentHandler) GetCollectionPreview(w http.ResponseWriter, r *http.Request) { collectionID := chi.URLParam(r, "id") siteID := r.URL.Query().Get("site_id") @@ -415,14 +415,24 @@ func (h *ContentHandler) GetCollectionTemplates(w http.ResponseWriter, r *http.R return } + // Get collection container + collection, err := h.repository.GetCollection(context.Background(), siteID, collectionID) + if err != nil { + http.Error(w, fmt.Sprintf("Collection not found: %v", err), http.StatusNotFound) + return + } + + // Get all templates for this collection templates, err := h.repository.GetCollectionTemplates(context.Background(), siteID, collectionID) if err != nil { - http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("Templates not found: %v", err), http.StatusInternalServerError) return } response := map[string]interface{}{ - "templates": templates, + "collection_id": collectionID, + "container_html": collection.ContainerHTML, + "templates": templates, } w.Header().Set("Content-Type", "application/json") @@ -525,10 +535,10 @@ func (h *ContentHandler) RegisterRoutes(r chi.Router) { // COLLECTION MANAGEMENT - Groups of related content // ============================================================================= r.Route("/collections", func(r chi.Router) { - r.Get("/", h.GetAllCollections) // GET /api/collections?site_id=X - r.Get("/{id}", h.GetCollection) // GET /api/collections/{id}?site_id=X - r.Get("/{id}/items", h.GetCollectionItems) // GET /api/collections/{id}/items?site_id=X - r.Get("/{id}/templates", h.GetCollectionTemplates) // GET /api/collections/{id}/templates?site_id=X + r.Get("/", h.GetAllCollections) // GET /api/collections?site_id=X + r.Get("/{id}", h.GetCollection) // GET /api/collections/{id}?site_id=X + r.Get("/{id}/items", h.GetCollectionItems) // GET /api/collections/{id}/items?site_id=X + r.Get("/{id}/preview", h.GetCollectionPreview) // GET /api/collections/{id}/preview?site_id=X // Protected routes r.Group(func(r chi.Router) { diff --git a/lib/src/core/api-client.js b/lib/src/core/api-client.js index be0cace..de12b14 100644 --- a/lib/src/core/api-client.js +++ b/lib/src/core/api-client.js @@ -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 of collection templates + * @returns {Promise} 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; } } diff --git a/lib/src/styles/insertr.css b/lib/src/styles/insertr.css index 8466a40..d09e428 100644 --- a/lib/src/styles/insertr.css +++ b/lib/src/styles/insertr.css @@ -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; + } +} diff --git a/lib/src/ui/collection-manager.js b/lib/src/ui/collection-manager.js index 1c12b5d..e1100d9 100644 --- a/lib/src/ui/collection-manager.js +++ b/lib/src/ui/collection-manager.js @@ -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 of template objects + * Get collection preview data (container + templates) + * @returns {Promise} 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} 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} 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 = ` -

Choose Template

-
- ${templates.map(template => ` -
-
- ${template.name}${template.is_default ? ' (default)' : ''} -
-
- ${this.createStyledTemplatePreview(template.html_template)} -
-
- `).join('')} +
+

Choose Template

+

Click on the item you want to add

-
+
+ ${previewHTML} +
+
-
`; - 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 '
Preview generation failed
'; + } + + // 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 '
Preview generation failed
'; + } + } } \ No newline at end of file