From f2bfcbc7d21cd46cb1451df8983c6781bbcf6e2c Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 1 Aug 2025 18:15:24 -0700 Subject: [PATCH] Enhance timeline editor left sidebar with collapsible content summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive linked content summary section with collapsible content type groups - Replace basic Timeline Stats with detailed content breakdown showing counts and items - Remove Import Events button to streamline Quick Actions - Implement Alpine.js methods for managing collapsible sections - Show up to 10 items per content type with "X more" indicators - Add Timeline Stats section below content summary with event count, word count, and total linked pages - Links open in new tabs for easy navigation - Maintain clean, organized left sidebar layout with consistent styling 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/controllers/timelines_controller.rb | 14 + app/views/timelines/edit.html.erb | 1910 ++++++++++++++--------- 2 files changed, 1186 insertions(+), 738 deletions(-) diff --git a/app/controllers/timelines_controller.rb b/app/controllers/timelines_controller.rb index c2563c2a..4ceaf72f 100644 --- a/app/controllers/timelines_controller.rb +++ b/app/controllers/timelines_controller.rb @@ -79,6 +79,7 @@ class TimelinesController < ApplicationController # Get content already linked to other events in this timeline @timeline_linked_content = {} + @timeline_content_summary = {} timeline_entity_ids = @timeline.timeline_events .joins(:timeline_event_entities) .pluck('timeline_event_entities.entity_type', 'timeline_event_entities.entity_id') @@ -89,9 +90,22 @@ class TimelinesController < ApplicationController content_class = content_class_from_name(entity_type) next unless content_class + # Original data for link modal @timeline_linked_content[entity_type] = content_class.where(id: entity_ids) .where(user: current_user) .limit(20) + + # Enhanced data for content summary sidebar + all_content = content_class.where(id: entity_ids) + .where(user: current_user) + .order(:name) + + @timeline_content_summary[entity_type] = { + count: all_content.count, + items: all_content.limit(10), # Show first 10, with expansion option + total_items: all_content.count, + content_class: content_class + } end end diff --git a/app/views/timelines/edit.html.erb b/app/views/timelines/edit.html.erb index 7bbf268e..2f6ec6a6 100644 --- a/app/views/timelines/edit.html.erb +++ b/app/views/timelines/edit.html.erb @@ -1,10 +1,10 @@ -
+
- -
-
- -
+ +
+
+ +
<%= link_to timelines_path, class: "inline-flex items-center text-gray-500 hover:text-gray-700 transition-colors" do %> @@ -31,7 +31,7 @@
-
+ + +
+ + + + +
+ +
+
+ +
+ +
+ + + <% if @timeline.timeline_events.any? %> + <% @timeline.timeline_events.includes(:timeline_event_entities).each_with_index do |event, index| %> +
+ + +
+ + +
+ +
+
+
+
+ <%= form_for event, html: { class: 'autosave-form' }, remote: true, url: "/plan/timeline_events/#{event.id}" do |f| %> + <%= f.text_field :title, + value: event.title.presence || "Untitled Event", + class: "text-xl font-semibold text-gray-900 bg-transparent border-0 border-b-2 border-transparent hover:border-gray-300 focus:border-green-500 focus:ring-0 w-full transition-all duration-200 placeholder-gray-400", + placeholder: "Event title...", + '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> + <% end %> +
+ + <% if event.time_label.present? || true %> +
+
+ <%= form_for event, html: { class: 'autosave-form' }, remote: true, url: "/plan/timeline_events/#{event.id}" do |f| %> + <%= f.text_field :time_label, + value: event.time_label, + class: "text-sm text-gray-600 bg-transparent border-0 border-b border-transparent hover:border-gray-300 focus:border-green-500 focus:ring-0 transition-all duration-200 placeholder-gray-400 w-full", + placeholder: "When does this start?", + '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> + <% end %> +
+
+ <%= form_for event, html: { class: 'autosave-form' }, remote: true, url: "/plan/timeline_events/#{event.id}" do |f| %> + <%= f.text_field :end_time_label, + value: event.end_time_label, + class: "text-sm text-gray-600 bg-transparent border-0 border-b border-transparent hover:border-gray-300 focus:border-green-500 focus:ring-0 transition-all duration-200 placeholder-gray-400 w-full", + placeholder: "When does this end?", + '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> + <% end %> +
+
+ <% end %> +
+ + + +
+
+ + +
+ +
+ <%= form_for event, html: { class: 'autosave-form' }, remote: true, url: "/plan/timeline_events/#{event.id}" do |f| %> + <%= f.text_area :description, + rows: 3, + class: "w-full text-gray-700 bg-transparent border border-gray-200 rounded-lg px-3 py-2 focus:border-green-500 focus:ring-2 focus:ring-green-500 focus:ring-opacity-20 transition-all duration-200 placeholder-gray-400 resize-none", + placeholder: "What happens in this event?", + '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> + <% end %> +
+ + + <%= render 'shared/timeline_event_linked_content', timeline_event: event %> +
+
+
+ <% end %> + <% else %> + +
+
+ timeline +
+

Your timeline is empty

+

+ Start building your timeline by adding your first event. Track important moments, plot points, and key developments in chronological order. +

+ +
+ <% end %> +
+
+
+
+ + + +
+ + + @@ -120,514 +622,107 @@
- <%= form_for @timeline, html: { class: 'timeline-meta-form' }, remote: true do |f| %> -
- -
- -
- - <%= f.select :universe_id, - options_from_collection_for_select(current_user.universes, :id, :name, @timeline.universe_id), - { include_blank: "No universe" }, - { class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500", - '@change': 'submitFormRemotely($el.form)' } %> -

Organize this timeline within a universe

+ <%= form_for @timeline, html: { class: 'timeline-meta-form' }, remote: true do |f| %> +
+ +
+ +
+ + <%= f.select :universe_id, + options_from_collection_for_select(current_user.universes, :id, :name, @timeline.universe_id), + { include_blank: "No universe" }, + { class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500", + '@change': 'submitFormRemotely($el.form)' } %> +

Organize this timeline within a universe

+
+ +
+ + <%= f.text_area :subtitle, + rows: 2, + class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 resize-none", + placeholder: "Brief description of this timeline...", + 'x-model': 'timelineData.subtitle', + '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> +
+ +
+ + <%= f.text_area :description, + rows: 4, + class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500", + placeholder: "Detailed description of this timeline...", + 'x-model': 'timelineData.description', + '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> +
-
- - <%= f.text_area :subtitle, - rows: 2, - class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 resize-none", - placeholder: "Brief description of this timeline...", - 'x-model': 'timelineData.subtitle', - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
- -
- - <%= f.text_area :description, - rows: 4, - class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500", - placeholder: "Detailed description of this timeline...", - 'x-model': 'timelineData.description', - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> + +
+
+ + <%= f.text_area :notes, + rows: 3, + class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500", + placeholder: "Public notes about this timeline...", + 'x-model': 'timelineData.notes', + '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> +
+ +
+ + <%= f.text_area :private_notes, + rows: 3, + class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500", + placeholder: "Private notes (only visible to you)...", + 'x-model': 'timelineData.private_notes', + '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> +
+ + +
+ +
+
+ <% @timeline.page_tags.each do |tag| %> + + <%= tag.tag %> + + + <% end %> +
+ +
+
- -
-
- - <%= f.text_area :notes, - rows: 3, - class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500", - placeholder: "Public notes about this timeline...", - 'x-model': 'timelineData.notes', - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
- -
- - <%= f.text_area :private_notes, - rows: 3, - class: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500", - placeholder: "Private notes (only visible to you)...", - 'x-model': 'timelineData.private_notes', - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
- - -
- -
-
- <% @timeline.page_tags.each do |tag| %> - - <%= tag.tag %> - - - <% end %> -
- + +
+
+
+

Danger Zone

+

These actions cannot be undone.

-
-
-
- - -
-
-
-

Danger Zone

-

These actions cannot be undone.

-
- <%= link_to timeline_path(@timeline), - method: :delete, - data: { confirm: "Are you sure you want to delete this timeline? This cannot be undone!" }, - class: "inline-flex items-center px-4 py-2 border border-red-300 text-sm font-medium rounded-lg text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" do %> - delete - Delete Timeline - <% end %> -
-
- <% end %> -
-
-
-
- - -
- -
-
-
-
- search -
- -
- - - - - <%= pluralize(@timeline.timeline_events.count, 'event') %> - -
- - -
- - - - - - -
- - <% @timeline.timeline_events.includes(:timeline_event_entities).each_with_index do |event, index| %> -
- -
- -
- -
-
-
- - -
-
- <%= link_to '#', class: 'js-move-event-to-top' do %> - keyboard_double_arrow_up - <% end %> - <%= link_to '#', class: 'js-move-event-up' do %> - keyboard_arrow_up - <% end %> - <%= link_to '#', class: 'js-move-event-down' do %> - keyboard_arrow_down - <% end %> - <%= link_to '#', class: 'js-move-event-to-bottom' do %> - keyboard_double_arrow_down - <% end %> -
-
-
- - -
- <%= form_for event, html: { class: 'autosave-form' }, remote: true do |f| %> - <%= f.hidden_field :timeline_id %> - -
- - -
-
- -
- <%= f.text_field :title, - class: "event-title w-full text-lg font-semibold text-gray-900 bg-transparent border-0 hover:bg-white hover:border hover:border-gray-200 focus:bg-white focus:border-green-500 focus:ring-0 transition-colors px-2 py-1 rounded", - placeholder: "Event title...", - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
- - -
- - -
-
-
- - -
- - -
-
- schedule -
-
- -
- - <%= f.text_field :time_label, - class: "timeline-input w-full text-sm font-medium text-gray-800", - placeholder: "Start time", - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
- - -
- arrow_forward -
- - -
- - <%= f.text_field :end_time_label, - class: "timeline-input w-full text-sm font-medium text-gray-800", - placeholder: "End time (optional)", - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
-
-
- - -
-
- - - <%= event.event_type_info[:icon] %> - <%= event.event_type_info[:name] %> - - - <% if event.importance_level == 'major' %> - - - star - Major - - <% end %> -
-
- - - -
- <%= f.text_area :description, - rows: 2, - class: "timeline-input w-full text-sm text-gray-700 resize-none", - placeholder: "What happened?", - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
- - -
- - <%= event.status_info[:name] %> - - -
- - - - - - - - -
-
- - -
- -
-

- settings - Event Settings -

-
- -
- - <%= f.select :event_type, - options_for_select(TimelineEvent::EVENT_TYPES.map { |k, v| [v[:name], k] }, event.event_type), - {}, - { class: "w-full text-xs px-2 py-1 border border-gray-300 rounded focus:ring-1 focus:ring-green-500 focus:border-green-500", - '@change': 'submitFormRemotely($el.form)' } %> -
- - -
- - <%= f.select :importance_level, - options_for_select(TimelineEvent::IMPORTANCE_LEVELS.map { |k, v| [v[:name], k] }, event.importance_level), - {}, - { class: "w-full text-xs px-2 py-1 border border-gray-300 rounded focus:ring-1 focus:ring-green-500 focus:border-green-500", - '@change': 'submitFormRemotely($el.form)' } %> -
-
-
- - -
-

- note - Notes -

-
- -
- - <%= f.text_area :notes, - rows: 2, - class: "w-full text-xs px-2 py-1 text-gray-600 bg-white border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 resize-none placeholder-gray-400", - placeholder: "Additional notes...", - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
- - -
- - <%= f.text_area :private_notes, - rows: 2, - class: "w-full text-xs px-2 py-1 text-gray-600 bg-amber-50 border border-amber-200 rounded focus:ring-1 focus:ring-amber-500 focus:border-amber-500 resize-none placeholder-amber-400", - placeholder: "Private notes...", - '@input.debounce.500ms': 'submitFormRemotely($el.form)' %> -
-
-
- - -
-

- check_circle - Status -

- <%= f.select :status, - options_for_select(TimelineEvent::STATUS_OPTIONS.map { |k, v| [v[:name], k] }, event.status), - {}, - { class: "w-full text-sm px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 bg-white", - '@change': 'submitFormRemotely($el.form)' } %> -
-
-
- - - <%= render 'shared/timeline_event_linked_content', timeline_event: event %> -
-
+ <%= link_to timeline_path(@timeline), + method: :delete, + data: { confirm: "Are you sure you want to delete this timeline? This cannot be undone!" }, + class: "inline-flex items-center px-4 py-2 border border-red-300 text-sm font-medium rounded-lg text-red-700 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" do %> + delete + Delete Timeline <% end %>
+ <% end %>
- <% end %> -
- - - - <% if @timeline.timeline_events.empty? %> -
-
- timeline -
-

Your timeline is empty

-

- Start building your timeline by adding your first event. Track important moments, plot points, and key developments in chronological order. -

-
- <% end %> +
@@ -976,158 +1071,6 @@
- - - @@ -1146,13 +1089,23 @@ function timelineEditor() { linkModalSearchQuery: '', selectedFilter: 'all', collapsedSections: {}, + contentSummaryCollapsed: {}, selectedEvents: [], + selectedEventId: null, + selectedEventData: null, autoSaveStatus: 'saved', titleFocused: false, eventSections: {}, eventTypeFilters: [], importanceFilters: [], statusFilters: [], + + // Reactive properties for real-time inspector panel updates + liveTitle: '', + liveTimeLabel: '', + liveEndTimeLabel: '', + liveDescription: '', + activeInputListeners: new Map(), // Track active listeners for cleanup timelineData: { name: '<%= j(@timeline.name) %>', subtitle: '<%= j(@timeline.subtitle) %>', @@ -1161,23 +1114,48 @@ function timelineEditor() { private_notes: '<%= j(@timeline.private_notes) %>' }, - // Initialize method with debugging + // Initialize method init() { - console.log('Timeline editor Alpine.js component initialized'); - console.log('Available methods:', Object.getOwnPropertyNames(this).filter(name => typeof this[name] === 'function')); - - // Test critical functions - console.log('Testing isEventSectionOpen function:', typeof this.isEventSectionOpen); - console.log('Testing linkableContentMatches function:', typeof this.linkableContentMatches); - console.log('Testing toggleEventSection function:', typeof this.toggleEventSection); - - // Make functions globally available for debugging - window.timelineEditorDebug = this; + // Add fallback click handlers for server-rendered events in case Alpine.js @click isn't working + setTimeout(() => { + const serverEventCards = document.querySelectorAll('.timeline-event-card:not(.js-template-event-card)'); + + serverEventCards.forEach((card) => { + const eventContainer = card.closest('[data-event-id]'); + if (eventContainer) { + const eventId = parseInt(eventContainer.getAttribute('data-event-id')); + + // Add a JavaScript click listener as fallback + card.addEventListener('click', (e) => { + // Check if Alpine.js handler already processed this + if (e.alpineProcessed) { + return; + } + + // Get event data from the card + const titleEl = card.querySelector('input[name*="[title]"]'); + const timeEl = card.querySelector('input[name*="[time_label]"]'); + const descEl = card.querySelector('textarea[name*="[description]"]'); + + const eventData = { + id: eventId, + title: titleEl ? titleEl.value : 'Untitled Event', + time_label: timeEl ? timeEl.value : '', + description: descEl ? descEl.value : '', + event_type: 'general', + importance_level: 'minor', + status: 'completed' + }; + + this.selectEvent(eventId, eventData); + }); + } + }); + }, 500); }, // Filter helper methods clearAllFilters() { - console.log('clearAllFilters called'); this.eventTypeFilters = []; this.importanceFilters = []; this.statusFilters = []; @@ -1241,9 +1219,72 @@ function timelineEditor() { return eventData.title.includes(query) || eventData.description.includes(query); }, + // Apply real-time filtering to timeline events + applyEventFilters() { + const events = document.querySelectorAll('.timeline-event-container:not(.timeline-event-template)'); + let visibleCount = 0; + + events.forEach(eventElement => { + const shouldShow = this.shouldShowEvent(eventElement); + + if (shouldShow) { + eventElement.style.display = 'block'; + eventElement.style.opacity = '1'; + eventElement.style.transform = 'translateY(0)'; + visibleCount++; + } else { + eventElement.style.opacity = '0'; + eventElement.style.transform = 'translateY(-10px)'; + setTimeout(() => { + if (!this.shouldShowEvent(eventElement)) { + eventElement.style.display = 'none'; + } + }, 200); + } + }); + + // Update empty state + this.updateEmptyState(visibleCount); + + return visibleCount; + }, + + updateEmptyState(visibleCount) { + const eventsContainer = document.querySelector('.timeline-events-container'); + let emptyState = eventsContainer.querySelector('.search-empty-state'); + + if (visibleCount === 0 && this.searchQuery.length > 0) { + // Show search empty state + if (!emptyState) { + emptyState = document.createElement('div'); + emptyState.className = 'search-empty-state text-center py-16'; + emptyState.innerHTML = ` +
+ search_off +
+

No events found

+

+ Try adjusting your search terms or clear the search to see all events. +

+ + `; + eventsContainer.appendChild(emptyState); + } + emptyState.style.display = 'block'; + } else { + // Hide search empty state + if (emptyState) { + emptyState.style.display = 'none'; + } + } + }, + // Link modal methods openLinkModal(eventId) { - console.log('openLinkModal called with eventId:', eventId); this.linkingEventId = eventId; this.showLinkModal = true; this.linkModalSearchQuery = ''; @@ -1257,7 +1298,6 @@ function timelineEditor() { }, closeLinkModal() { - console.log('closeLinkModal called'); this.showLinkModal = false; this.linkingEventId = null; this.linkModalSearchQuery = ''; @@ -1267,18 +1307,24 @@ function timelineEditor() { }, toggleSection(sectionKey) { - console.log('toggleSection called with key:', sectionKey); this.collapsedSections[sectionKey] = !this.collapsedSections[sectionKey]; }, isSectionCollapsed(sectionKey) { - const result = this.collapsedSections[sectionKey] || false; - console.log('isSectionCollapsed called with key:', sectionKey, 'result:', result); - return result; + return this.collapsedSections[sectionKey] || false; + }, + + // Content summary section management + toggleContentSummarySection(contentType) { + this.contentSummaryCollapsed[contentType] = !this.contentSummaryCollapsed[contentType]; + }, + + isContentSummaryCollapsed(contentType) { + // Default to expanded (false) on first load + return this.contentSummaryCollapsed[contentType] || false; }, linkableContentMatches(contentName, contentType) { - console.log('linkableContentMatches called with:', contentName, contentType); // First check filter selection if (this.selectedFilter && this.selectedFilter !== 'all') { @@ -1324,43 +1370,350 @@ function timelineEditor() { // Event section management toggleEventSection(eventId, section) { const key = `${eventId}_${section}`; - console.log('toggleEventSection called:', eventId, section, 'key:', key); this.eventSections[key] = !this.eventSections[key]; }, isEventSectionOpen(eventId, section) { const key = `${eventId}_${section}`; - const result = this.eventSections[key] || false; - console.log('isEventSectionOpen called:', eventId, section, 'key:', key, 'result:', result); - return result; + return this.eventSections[key] || false; + }, + + // Event selection for inspector panel + selectEvent(eventId, eventData) { + this.selectedEventId = eventId; + this.selectedEventData = eventData; + + // Set up live form listeners for real-time inspector panel updates + this.setupLiveFormListeners(eventId); + + // Update sidebar linked content + this.updateSidebarLinkedContent(eventId); + + // Scroll inspector panel to top + const inspectorContent = document.querySelector('.w-96 .overflow-y-auto'); + if (inspectorContent) { + inspectorContent.scrollTop = 0; + } + }, + + clearEventSelection() { + this.selectedEventId = null; + this.selectedEventData = null; + this.clearInputListeners(); + }, + + // Set up real-time input listeners for inspector panel updates + setupLiveFormListeners(eventId) { + // Clear any existing listeners first + this.clearInputListeners(); + + // Find form inputs for this event + const eventContainer = document.querySelector(`[data-event-id="${eventId}"]`); + if (!eventContainer) { + return; + } + + const titleInput = eventContainer.querySelector('input[name*="[title]"]'); + const timeLabelInput = eventContainer.querySelector('input[name*="[time_label]"]'); + const endTimeLabelInput = eventContainer.querySelector('input[name*="[end_time_label]"]'); + const descriptionTextarea = eventContainer.querySelector('textarea[name*="[description]"]'); + + // Set initial values + this.liveTitle = titleInput?.value || 'Untitled Event'; + this.liveTimeLabel = timeLabelInput?.value || ''; + this.liveEndTimeLabel = endTimeLabelInput?.value || ''; + this.liveDescription = descriptionTextarea?.value || ''; + + // Create and store event listeners + const listeners = []; + + if (titleInput) { + const titleListener = (e) => { + this.liveTitle = e.target.value || 'Untitled Event'; + }; + titleInput.addEventListener('input', titleListener); + listeners.push({ element: titleInput, type: 'input', listener: titleListener }); + } + + if (timeLabelInput) { + const timeLabelListener = (e) => { + this.liveTimeLabel = e.target.value || ''; + }; + timeLabelInput.addEventListener('input', timeLabelListener); + listeners.push({ element: timeLabelInput, type: 'input', listener: timeLabelListener }); + } + + if (endTimeLabelInput) { + const endTimeLabelListener = (e) => { + this.liveEndTimeLabel = e.target.value || ''; + }; + endTimeLabelInput.addEventListener('input', endTimeLabelListener); + listeners.push({ element: endTimeLabelInput, type: 'input', listener: endTimeLabelListener }); + } + + if (descriptionTextarea) { + const descriptionListener = (e) => { + this.liveDescription = e.target.value || ''; + }; + descriptionTextarea.addEventListener('input', descriptionListener); + listeners.push({ element: descriptionTextarea, type: 'input', listener: descriptionListener }); + } + + // Store listeners for cleanup + this.activeInputListeners.set(eventId, listeners); + }, + + // Clear all active input listeners + clearInputListeners() { + this.activeInputListeners.forEach((listeners, eventId) => { + listeners.forEach(({ element, type, listener }) => { + element.removeEventListener(type, listener); + }); + }); + + this.activeInputListeners.clear(); + + // Reset live data + this.liveTitle = ''; + this.liveTimeLabel = ''; + this.liveEndTimeLabel = ''; + this.liveDescription = ''; + }, + + // Update sidebar linked content + updateSidebarLinkedContent(eventId) { + const sidebarContainer = document.getElementById('sidebar-linked-content'); + if (!sidebarContainer) { + return; + } + + // Find the main timeline linked content for this event + const mainLinkedContent = document.querySelector(`#linked-content-${eventId}`); + if (!mainLinkedContent) { + // Show empty state + sidebarContainer.innerHTML = ` +
+ info + Linked content will appear here when you connect pages to this event +
+ `; + return; + } + + // Extract linked entities from the main content + const linkedCards = mainLinkedContent.querySelectorAll('.linked-content-card'); + + if (linkedCards.length === 0) { + // Show empty state + sidebarContainer.innerHTML = ` +
+ info + Linked content will appear here when you connect pages to this event +
+ `; + return; + } + + // Build compact vertical list + let sidebarHTML = ''; + linkedCards.forEach(card => { + const nameLink = card.querySelector('.type-badge a'); + const icon = card.querySelector('.type-badge i'); + const removeButton = card.querySelector('.remove-btn'); + + if (nameLink && icon) { + const name = nameLink.textContent.trim(); + const href = nameLink.getAttribute('href'); + const iconClass = icon.className; + const iconText = icon.textContent; + const removeHref = removeButton ? removeButton.getAttribute('href') : ''; + + sidebarHTML += ` +
+
+ ${iconText} + + ${name} + +
+ ${removeButton ? ` + + ` : ''} +
+ `; + } + }); + + sidebarContainer.innerHTML = sidebarHTML; + }, + + // Computed properties for live form data reading (now using reactive data with DOM fallback) + get selectedEventTitle() { + if (!this.selectedEventId) return ''; + + // Use reactive data if available (updated by input listeners) + if (this.liveTitle) { + return this.liveTitle; + } + + // Fallback to DOM query for initial load + const input = document.querySelector(`[data-event-id="${this.selectedEventId}"] input[name*="[title]"]`); + return input?.value || 'Untitled Event'; + }, + + get selectedEventTimeLabel() { + if (!this.selectedEventId) return ''; + + // Use reactive data if available (updated by input listeners) + if (this.liveTimeLabel !== '') { + return this.liveTimeLabel; + } + + // Fallback to DOM query for initial load + const input = document.querySelector(`[data-event-id="${this.selectedEventId}"] input[name*="[time_label]"]`); + return input?.value || ''; + }, + + get selectedEventEndTimeLabel() { + if (!this.selectedEventId) return ''; + + // Use reactive data if available (updated by input listeners) + if (this.liveEndTimeLabel !== '') { + return this.liveEndTimeLabel; + } + + // Fallback to DOM query for initial load + const input = document.querySelector(`[data-event-id="${this.selectedEventId}"] input[name*="[end_time_label]"]`); + return input?.value || ''; + }, + + get selectedEventDescription() { + if (!this.selectedEventId) return ''; + + // Use reactive data if available (updated by input listeners) + if (this.liveDescription !== '') { + return this.liveDescription; + } + + // Fallback to DOM query for initial load + const textarea = document.querySelector(`[data-event-id="${this.selectedEventId}"] textarea[name*="[description]"]`); + return textarea?.value || ''; + }, + + get selectedEventType() { + if (!this.selectedEventId) return 'general'; + // For now, return from static data since event_type isn't in the form yet + return this.selectedEventData?.event_type || 'general'; + }, + + get selectedEventImportance() { + if (!this.selectedEventId) return 'minor'; + // For now, return from static data since importance_level isn't in the form yet + return this.selectedEventData?.importance_level || 'minor'; } }; } -console.log('Timeline editor function defined'); // Helper function to submit forms remotely function submitFormRemotely(form) { + if (form.getAttribute('data-remote') !== 'true') { form.setAttribute('data-remote', 'true'); } + // Use Rails UJS to submit the form remotely if (typeof Rails !== 'undefined' && Rails.fire) { Rails.fire(form, 'submit'); } else { - // Fallback: dispatch submit event that Rails UJS should catch - const event = document.createEvent('HTMLEvents'); - event.initEvent('submit', true, true); - form.dispatchEvent(event); + // Manual AJAX implementation since Rails UJS is not working + submitFormWithFetch(form); } } + +// Manual AJAX form submission function +function submitFormWithFetch(form) { + + // Set auto-save status to saving + const alpineEl = document.querySelector('[x-data]'); + if (alpineEl) { + Alpine.$data(alpineEl).autoSaveStatus = 'saving'; + } + + const formData = new FormData(form); + const url = form.action; + const method = form.method.toUpperCase(); + + // Add Rails authenticity token if not present + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + if (csrfToken && !formData.has('authenticity_token')) { + formData.append('authenticity_token', csrfToken); + } + + + fetch(url, { + method: method, + body: formData, + headers: { + 'X-CSRF-Token': csrfToken, + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + }) + .then(data => { + + // Trigger success event manually + const successEvent = new CustomEvent('ajax:success', { + detail: [data], + bubbles: true, + cancelable: true + }); + form.dispatchEvent(successEvent); + + // Update auto-save status + if (alpineEl) { + Alpine.$data(alpineEl).autoSaveStatus = 'saved'; + setTimeout(() => { + Alpine.$data(alpineEl).autoSaveStatus = 'saved'; + }, 2000); + } + }) + .catch(error => { + + // Trigger error event manually + const errorEvent = new CustomEvent('ajax:error', { + detail: [null, { status: 'error', message: error.message }], + bubbles: true, + cancelable: true + }); + form.dispatchEvent(errorEvent); + + // Update auto-save status + if (alpineEl) { + Alpine.$data(alpineEl).autoSaveStatus = 'error'; + } + }); +} - + \ No newline at end of file