+
<%= @timeline.timeline_events.count %> events
•
<%= number_with_delimiter(@timeline.cached_word_count) %> words
@@ -39,19 +39,19 @@
- <%= link_to timeline_path(@timeline), class: "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 shadow-sm" do %>
+ <%= link_to timeline_path(@timeline), class: "inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 shadow-sm transition-colors" do %>
visibility
View
<% end %>
@@ -59,29 +59,531 @@
-
- <%= form_for @timeline, html: { class: 'timeline-meta-form', '@submit': 'autoSaveStatus = "saving"' }, remote: true do |f| %>
-
- <%= f.text_field :name,
- class: "text-3xl font-bold 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 pr-10",
- placeholder: "Timeline title...",
- 'x-model': 'timelineData.name',
- '@input.debounce.500ms': 'submitFormRemotely($el.form)',
- '@focus': 'titleFocused = true',
- '@blur': 'titleFocused = false' %>
-
-
-
- edit
-
-
-
-
-
/255
+ <%= form_for @timeline, html: { class: 'timeline-meta-form', '@submit': 'autoSaveStatus = "saving"' }, remote: true do |f| %>
+
+ <%= f.text_field :name,
+ class: "text-2xl font-bold 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 pr-10",
+ placeholder: "Timeline title...",
+ 'x-model': 'timelineData.name',
+ '@input.debounce.500ms': 'submitFormRemotely($el.form)',
+ '@focus': 'titleFocused = true',
+ '@blur': 'titleFocused = false' %>
+
+
+
+ edit
+
+
+ <% end %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% if @timeline.timeline_events.any? %>
+ <% @timeline.timeline_events.includes(:timeline_event_entities).each_with_index do |event, index| %>
+
+
+
+
+
+
+
, {
+ id: <%= event.id %>,
+ title: '<%= j(event.title) %>',
+ time_label: '<%= j(event.time_label) %>',
+ description: '<%= j(event.description) %>',
+ event_type: '<%= event.event_type || 'general' %>',
+ importance_level: '<%= event.importance_level || 'minor' %>',
+ status: '<%= event.status || 'completed' %>'
+ })"
+ :class="{ 'ring-2 ring-green-500 ring-opacity-50': selectedEventId === <%= event.id %> }">
+
+
+
+
+
+ <%= 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 %>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <%= pluralize(@timeline.timeline_events.count, 'event') %>
-
-
-
-
-
-
-
-
-
-
-
-
Event Types
-
- <% TimelineEvent::EVENT_TYPES.each do |type_key, type_info| %>
-
- <% end %>
-
-
-
-
-
-
Importance Level
-
- <% TimelineEvent::IMPORTANCE_LEVELS.each do |level_key, level_info| %>
-
- <% end %>
-
-
-
-
-
-
Status
-
- <% TimelineEvent::STATUS_OPTIONS.each do |status_key, status_info| %>
-
- <% end %>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <% @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 @@
-
-
-
- <% event = TimelineEvent.new %>
-
-
-
-
-
-
-
-
-
- <%= 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, method: :patch do |f| %>
- <%= f.hidden_field :timeline_id, value: @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..." %>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
schedule
-
- <%= f.text_field :time_label,
- class: "text-base font-medium text-green-600 bg-transparent border-0 border-b border-transparent hover:border-green-300 focus:border-green-500 focus:ring-0 placeholder-green-400 transition-colors",
- placeholder: "Start time" %>
- arrow_forward
- <%= f.text_field :end_time_label,
- class: "text-base font-medium text-green-600 bg-transparent border-0 border-b border-transparent hover:border-green-300 focus:border-green-500 focus:ring-0 placeholder-green-400 transition-colors",
- placeholder: "End time (optional)" %>
-
-
-
-
-
-
- event
- General Event
-
-
-
-
- <%= f.text_field :title,
- class: "event-title text-lg font-semibold text-gray-900 bg-transparent border-0 focus:ring-0 w-full placeholder-gray-400 mb-2",
- placeholder: "Event title..." %>
-
-
-
- <%= f.text_area :description,
- rows: 1,
- class: "w-full text-sm text-gray-700 bg-transparent border-0 focus:ring-0 resize-none placeholder-gray-400 transition-all",
- placeholder: "What happened?",
- style: "min-height: 1.5rem;" %>
-
-
-
-
-
- Completed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- <% end %>
-
-
-
-
@@ -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 += `
+
+
+ ${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