diff --git a/internal/db/http_client.go b/internal/db/http_client.go index 12d347f..3606445 100644 --- a/internal/db/http_client.go +++ b/internal/db/http_client.go @@ -204,6 +204,10 @@ func (c *HTTPClient) CreateCollectionItemAtomic(ctx context.Context, siteID, col return nil, fmt.Errorf("collection operations not implemented in HTTPClient") } +func (c *HTTPClient) GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error) { + return 0, fmt.Errorf("collection operations not implemented in HTTPClient") +} + func (c *HTTPClient) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) { return nil, fmt.Errorf("content update operations not implemented in HTTPClient") } diff --git a/internal/db/postgresql_repository.go b/internal/db/postgresql_repository.go index d96e1ac..32976d8 100644 --- a/internal/db/postgresql_repository.go +++ b/internal/db/postgresql_repository.go @@ -282,6 +282,23 @@ func (r *PostgreSQLRepository) CreateCollectionItemAtomic(ctx context.Context, s return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for PostgreSQL") } +// GetMaxPosition returns the maximum position for items in a collection +func (r *PostgreSQLRepository) GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error) { + result, err := r.queries.GetMaxPosition(ctx, postgresql.GetMaxPositionParams{ + CollectionID: collectionID, + SiteID: siteID, + }) + if err != nil { + return 0, err + } + + // Convert interface{} to int (PostgreSQL returns int64) + if maxPos, ok := result.(int64); ok { + return int(maxPos), nil + } + return 0, nil +} + // UpdateContent updates an existing content item func (r *PostgreSQLRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) { content, err := r.queries.UpdateContent(ctx, postgresql.UpdateContentParams{ diff --git a/internal/db/repository.go b/internal/db/repository.go index 058bc98..235a948 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -22,6 +22,7 @@ type ContentRepository interface { CreateCollectionTemplate(ctx context.Context, siteID, collectionID, name, htmlTemplate string, isDefault bool) (*CollectionTemplateItem, error) CreateCollectionItem(ctx context.Context, siteID, collectionID, itemID string, templateID int, htmlContent string, position int, lastEditedBy string) (*CollectionItemWithTemplate, error) CreateCollectionItemAtomic(ctx context.Context, siteID, collectionID string, templateID int, lastEditedBy string) (*CollectionItemWithTemplate, error) + GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error) ReorderCollectionItems(ctx context.Context, siteID, collectionID string, items []CollectionItemPosition, lastEditedBy string) error // Transaction support diff --git a/internal/db/sqlite_repository.go b/internal/db/sqlite_repository.go index b702a37..bf8fd21 100644 --- a/internal/db/sqlite_repository.go +++ b/internal/db/sqlite_repository.go @@ -287,6 +287,23 @@ func (r *SQLiteRepository) CreateCollectionItemAtomic(ctx context.Context, siteI return nil, fmt.Errorf("CreateCollectionItemAtomic not yet implemented for SQLite") } +// GetMaxPosition returns the maximum position for items in a collection +func (r *SQLiteRepository) GetMaxPosition(ctx context.Context, siteID, collectionID string) (int, error) { + result, err := r.queries.GetMaxPosition(ctx, sqlite.GetMaxPositionParams{ + CollectionID: collectionID, + SiteID: siteID, + }) + if err != nil { + return 0, err + } + + // Convert interface{} to int (SQLite returns int64) + if maxPos, ok := result.(int64); ok { + return int(maxPos), nil + } + return 0, nil +} + // UpdateContent updates an existing content item func (r *SQLiteRepository) UpdateContent(ctx context.Context, siteID, contentID, htmlContent, lastEditedBy string) (*ContentItem, error) { content, err := r.queries.UpdateContent(ctx, sqlite.UpdateContentParams{ diff --git a/internal/engine/collection.go b/internal/engine/collection.go index 1db91cf..6b08f7e 100644 --- a/internal/engine/collection.go +++ b/internal/engine/collection.go @@ -246,12 +246,9 @@ func (e *ContentEngine) processChildElementsAsContent(childElement *html.Node, s // Set the data-content-id attribute SetAttribute(n, "data-content-id", contentID) - // Clear content - this will be hydrated during reconstruction - for child := n.FirstChild; child != nil; { - next := child.NextSibling - n.RemoveChild(child) - child = next - } + // Keep content for initial display - don't clear it + // The content is already stored in the database and will be available for editing + // Preserving content ensures elements have height and are clickable } }) @@ -271,12 +268,8 @@ func (e *ContentEngine) generateStructuralTemplateFromChild(childElement *html.N // Set the data-content-id attribute SetAttribute(n, "data-content-id", contentEntries[entryIndex].ID) - // Clear content - this will be hydrated during reconstruction - for child := n.FirstChild; child != nil; { - next := child.NextSibling - n.RemoveChild(child) - child = next - } + // Keep content for structural template - ensures elements have height and are clickable + // Content is stored separately in database for editing entryIndex++ } @@ -352,9 +345,17 @@ func (e *ContentEngine) CreateCollectionItemFromTemplate( return nil, fmt.Errorf("failed to generate structural template: %w", err) } - // Create collection item with structural template + // Get next position to place new item at the end of collection + maxPosition, err := e.client.GetMaxPosition(context.Background(), siteID, collectionID) + if err != nil { + return nil, fmt.Errorf("failed to get max position for collection %s: %w", collectionID, err) + } + nextPosition := maxPosition + 1 + fmt.Printf("🔢 Max position for collection %s: %d, assigning new item position: %d\n", collectionID, maxPosition, nextPosition) + + // Create collection item with structural template at end position collectionItem, err := e.client.CreateCollectionItem(context.Background(), - siteID, collectionID, itemID, templateID, structuralTemplate, 0, lastEditedBy, + siteID, collectionID, itemID, templateID, structuralTemplate, nextPosition, lastEditedBy, ) if err != nil { return nil, fmt.Errorf("failed to create collection item: %w", err) diff --git a/lib/src/styles/insertr.css b/lib/src/styles/insertr.css index ab96ba6..8466a40 100644 --- a/lib/src/styles/insertr.css +++ b/lib/src/styles/insertr.css @@ -814,6 +814,176 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after { border-radius: var(--insertr-border-radius); } +/* ================================================================= + TEMPLATE SELECTION MODAL STYLES + ================================================================= */ + +/* Template selection modal container */ +.insertr-template-selector { + background: white; + border-radius: 8px; + padding: 24px; + max-width: 600px; + width: 95%; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + font-family: var(--insertr-font-family); + margin: 20px; +} + +.insertr-template-selector h3 { + margin: 0 0 16px 0; + font-size: 18px; + font-weight: 600; + color: var(--insertr-text-primary); +} + +/* Template options container */ +.insertr-template-options { + margin-bottom: 20px; +} + +/* Individual template option */ +.insertr-template-option { + border: 2px solid var(--insertr-border-color); + border-radius: 6px; + padding: 12px; + margin-bottom: 12px; + cursor: pointer; + transition: all 0.2s ease; + background: var(--insertr-bg-primary); +} + +/* Default template styling - subtle indicator only */ +.insertr-template-option.insertr-template-default { + border-left: 3px solid var(--insertr-primary); + background: #f8fafc; +} + +/* Selected template styling - prominent selection indicator */ +.insertr-template-option.insertr-template-selected { + border-color: var(--insertr-primary) !important; + background: #eff6ff !important; + box-shadow: 0 0 0 2px var(--insertr-primary) !important; +} + +/* Default template when selected - override default styling */ +.insertr-template-option.insertr-template-default.insertr-template-selected { + border-left: 2px solid var(--insertr-primary) !important; + border-color: var(--insertr-primary) !important; + background: #eff6ff !important; + box-shadow: 0 0 0 2px var(--insertr-primary) !important; +} + +/* Hover state for template options (not selected) */ +.insertr-template-option:hover:not(.insertr-template-selected) { + border-color: var(--insertr-text-secondary); + background: var(--insertr-bg-secondary); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Default template hover (when not selected) */ +.insertr-template-option.insertr-template-default:hover:not(.insertr-template-selected) { + background: #f1f5f9; + border-left-color: var(--insertr-primary-hover); +} + +/* Selected template hover - enhance the selected state */ +.insertr-template-option.insertr-template-selected:hover { + background: #dbeafe !important; + border-color: var(--insertr-primary-hover) !important; + box-shadow: 0 0 0 2px var(--insertr-primary-hover) !important; +} + +/* Template name and info */ +.insertr-template-name { + font-weight: 500; + margin-bottom: 4px; + color: var(--insertr-text-primary); +} + +/* Template preview container */ +.insertr-template-preview-container { + background: var(--insertr-bg-secondary); + border: 1px solid var(--insertr-border-color); + border-radius: 4px; + padding: 12px; + overflow: hidden; + max-height: 120px; + position: relative; +} + +/* Styled template preview - preserves original styling */ +.insertr-template-preview-render { + pointer-events: none; + overflow: hidden; + max-height: 100px; +} + +/* Style placeholder content in previews */ +.insertr-preview-content { + display: inline-block !important; +} + +/* Fallback preview for errors */ +.insertr-template-preview-fallback { + font-size: 13px; + color: var(--insertr-text-secondary); + font-family: var(--insertr-font-family); + font-style: italic; + padding: 8px; +} + +/* Modal actions */ +.insertr-template-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid var(--insertr-border-color); +} + +.insertr-template-btn { + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-family: var(--insertr-font-family); + font-size: var(--insertr-font-size-base); + font-weight: 500; + transition: all 0.2s ease; +} + +.insertr-template-btn-cancel { + border: 1px solid var(--insertr-border-color); + background: var(--insertr-bg-primary); + color: var(--insertr-text-primary); +} + +.insertr-template-btn-cancel:hover { + background: var(--insertr-bg-secondary); + border-color: var(--insertr-text-secondary); +} + +.insertr-template-btn-select { + background: var(--insertr-primary); + color: white; + border: none; + opacity: 0.5; + cursor: not-allowed; +} + +.insertr-template-btn-select:not(:disabled) { + opacity: 1; + cursor: pointer; +} + +.insertr-template-btn-select:not(:disabled):hover { + background: var(--insertr-primary-hover); +} + /* Add button positioned in top right of container */ .insertr-add-btn { position: absolute; diff --git a/lib/src/ui/collection-manager.js b/lib/src/ui/collection-manager.js index 205fbe4..1c12b5d 100644 --- a/lib/src/ui/collection-manager.js +++ b/lib/src/ui/collection-manager.js @@ -509,117 +509,86 @@ export class CollectionManager { // Create modal overlay const overlay = document.createElement('div'); overlay.className = 'insertr-modal-overlay'; - overlay.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 999999; - `; // Create modal content const modal = document.createElement('div'); modal.className = 'insertr-template-selector'; - modal.style.cssText = ` - background: white; - border-radius: 8px; - padding: 24px; - max-width: 500px; - width: 90%; - max-height: 70vh; - overflow-y: auto; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); - `; - // Create modal HTML + // Create modal HTML using CSS classes modal.innerHTML = ` -