PageSourceSearch

https://naperastro.org/wp-content/plugins/naperastro-plugin/assets/videos.js?ver=7.1.2

js naperastro.org collected 2026-10-04 20:26:42 UTC 13,887 bytes, 452 lines download raw bytes

1function videos() {
2  return {
3    allVideos: [],
4    playlists: [],
5    statistics: null,
6    loading: true,
7    error: null,
8
9    // Filters and Search
10    searchQuery: "",
11    selectedPlaylist: "",
12    sortBy: "date_desc",
13
14    // Load More functionality
15    videosPerLoad: 12,
16    currentlyDisplayed: 12,
17
18    // UI State
19    activeVideo: null,
20    isFilterOpen: false,
21    isSearching: false,
22    searchFocused: false,
23    showDescription: true,
24
25    // Settings (will be overridden by WordPress config)
26    showPlaylists: true,
27    showStatistics: true,
28
29    // Introduction Video (will be overridden by WordPress config)
30    introVideoId: "wSz0h2c4TCo",
31    showIntroVideo: false,
32    showIntroBanner: true,
33
34    // Computed Properties
35    get filteredVideos() {
36      let videos = [...this.allVideos];
37
38      // Apply search filter
39      if (this.searchQuery) {
40        const query = this.searchQuery.toLowerCase();
41        videos = videos.filter(
42          (video) =>
43            video.title.toLowerCase().includes(query) ||
44            (video.description &&
45              video.description.toLowerCase().includes(query)),
46        );
47      }
48
49      // Apply playlist filter
50      if (this.selectedPlaylist) {
51        videos = videos.filter(
52          (video) =>
53            video.playlists &&
54            video.playlists.some(
55              (playlist) => playlist.id === this.selectedPlaylist,
56            ),
57        );
58      }
59
60      // Apply sorting
61      videos.sort((a, b) => {
62        switch (this.sortBy) {
63          case "date_desc":
64            return new Date(b.published_at) - new Date(a.published_at);
65          case "date_asc":
66            return new Date(a.published_at) - new Date(b.published_at);
67          case "views_desc":
68            return (b.view_count || 0) - (a.view_count || 0);
69          case "views_asc":
70            return (a.view_count || 0) - (b.view_count || 0);
71          case "title_asc":
72            return a.title.localeCompare(b.title);
73          case "title_desc":
74            return b.title.localeCompare(a.title);
75          default:
76            return 0;
77        }
78      });
79
80      return videos;
81    },
82
83    get displayedVideos() {
84      return this.filteredVideos.slice(0, this.currentlyDisplayed);
85    },
86
87    get hasMoreVideos() {
88      return this.filteredVideos.length > this.currentlyDisplayed;
89    },
90
91    // Methods
92    async init() {
93      this.loadWordPressConfig();
94      this.checkIntroVideoVisibility();
95      await this.loadVideos();
96    },
97
98    // Load configuration from WordPress
99    loadWordPressConfig() {
100      if (typeof naperAstroVideosConfig !== 'undefined') {
101        this.videosPerLoad = naperAstroVideosConfig.videosPerPage || this.videosPerLoad;
102        this.showPlaylists = naperAstroVideosConfig.showPlaylists !== undefined ? naperAstroVideosConfig.showPlaylists : this.showPlaylists;
103        this.showStatistics = naperAstroVideosConfig.showStatistics !== undefined ? naperAstroVideosConfig.showStatistics : this.showStatistics;
104        this.introVideoId = naperAstroVideosConfig.introVideoId || this.introVideoId;
105        this.showIntroBanner = naperAstroVideosConfig.showIntroBanner !== undefined ? naperAstroVideosConfig.showIntroBanner : this.showIntroBanner;
106      }
107    },
108
109    async loadVideos() {
110      try {
111        this.loading = true;
112        this.error = null;
113
114        const response = await fetch("/wp-json/naperastro/v1/videos/optimized");
115        const data = await response.json();
116
117        if (!data.success) {
118          throw new Error(data.error?.message || "Failed to load videos");
119        }
120
121        this.allVideos = data.videos || [];
122        this.playlists = data.playlists || [];
123        this.statistics = data.statistics || null;
124
125        console.log("Videos loaded:", this.allVideos.length);
126        if (data.api_efficiency) {
127          console.log("API Efficiency:", data.api_efficiency);
128        }
129      } catch (err) {
130        console.error("Error loading videos:", err);
131        this.error = err.message;
132      } finally {
133        this.loading = false;
134      }
135    },
136
137    applyFilters() {
138      // Show searching state
139      this.isSearching = true;
140
141      // Reset displayed count when filters change
142      this.currentlyDisplayed = this.videosPerLoad;
143
144      // Clear searching state after a short delay
145      setTimeout(() => {
146        this.isSearching = false;
147      }, 300);
148    },
149
150    clearFilters() {
151      this.searchQuery = "";
152      this.selectedPlaylist = "";
153      this.currentlyDisplayed = this.videosPerLoad;
154      this.isSearching = false;
155    },
156
157    loadMore() {
158      this.currentlyDisplayed += this.videosPerLoad;
159    },
160
161    openVideo(videoId) {
162      // Track video open analytics
163      this.trackVideoEngagement(videoId, 'opened', 'grid');
164      
165      this.activeVideo = videoId;
166      this.showDescription = true; // Always show description when opening a video
167      document.body.style.overflow = "hidden";
168
169      // Focus management for accessibility
170      this.$nextTick(() => {
171        const toggleButton = document.querySelector(
172          '[aria-label="Toggle video description"]',
173        );
174        if (toggleButton) {
175          toggleButton.focus();
176        }
177      });
178    },
179
180    closeVideo() {
181      this.activeVideo = null;
182      this.showDescription = true; // Reset to always show description
183      document.body.style.overflow = "";
184
185      // Return focus to the video card that was clicked
186      this.$nextTick(() => {
187        const videoCard = document.querySelector(
188          `[data-video-id="${this.activeVideo}"]`,
189        );
190        if (videoCard) {
191          videoCard.focus();
192        }
193      });
194    },
195
196    // Introduction Video Methods
197    checkIntroVideoVisibility() {
198      // Check for URL parameter to reset intro video visibility
199      const urlParams = new URLSearchParams(window.location.search);
200      const resetIntro = urlParams.get('reset_intro');
201      
202      if (resetIntro === 'true') {
203        localStorage.removeItem('naperastro_intro_dismissed');
204        localStorage.removeItem('naperastro_intro_watched');
205        // Remove the parameter from URL without page reload
206        const newUrl = new URL(window.location);
207        newUrl.searchParams.delete('reset_intro');
208        window.history.replaceState({}, '', newUrl);
209      }
210      
211      const dismissed = localStorage.getItem('naperastro_intro_dismissed');
212      const watched = localStorage.getItem('naperastro_intro_watched');
213      
214      // Show intro video if banner is enabled and it hasn't been dismissed and hasn't been watched
215      this.showIntroVideo = this.showIntroBanner && !dismissed && !watched;
216    },
217
218    openIntroVideo() {
219      // Track analytics for intro video click
220      if (typeof gtag !== 'undefined') {
221        gtag('event', 'intro_video_clicked', {
222          'video_id': this.introVideoId,
223          'event_category': 'video_engagement',
224          'event_label': 'introduction_banner'
225        });
226      }
227      
228      // Mark as watched
229      localStorage.setItem('naperastro_intro_watched', 'true');
230      localStorage.setItem('naperastro_intro_watched_date', new Date().toISOString());
231      this.showIntroVideo = false;
232      
233      // Open the intro video
234      this.openVideo(this.introVideoId);
235    },
236
237    dismissIntroVideo() {
238      // Track analytics for intro video dismissal
239      if (typeof gtag !== 'undefined') {
240        gtag('event', 'intro_video_dismissed', {
241          'video_id': this.introVideoId,
242          'event_category': 'video_engagement',
243          'event_label': 'introduction_banner'
244        });
245      }
246      
247      // Mark as dismissed
248      localStorage.setItem('naperastro_intro_dismissed', 'true');
249      localStorage.setItem('naperastro_intro_dismissed_date', new Date().toISOString());
250      this.showIntroVideo = false;
251    },
252
253    getPlaylistTitle(playlistId) {
254      const playlist = this.playlists.find((p) => p.id === playlistId);
255      return playlist ? playlist.title : playlistId;
256    },
257
258    toggleDescription() {
259      this.showDescription = !this.showDescription;
260      
261      // On mobile, scroll to ensure description is visible when toggled to show
262        if (this.showDescription && window.innerWidth <= 768) {
263          this.$nextTick(() => {
264            const descriptionArea = document.querySelector('.naperastro-video-description-area');
265            if (descriptionArea) {
266              descriptionArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
267            }
268          });
269        }
270    },
271
272    getActiveVideoData() {
273      return this.allVideos.find(video => video.id === this.activeVideo);
274    },
275
276    // Check if device is mobile
277    isMobile() {
278      return window.innerWidth <= 768;
279    },
280
281    // Handle orientation change
282    handleOrientationChange() {
283      if (this.activeVideo) {
284        // Delay to allow for orientation change to complete
285        setTimeout(() => {
286          const modal = document.querySelector('.naperastro-video-modal');
287          if (modal && this.isMobile()) {
288            modal.scrollTop = 0;
289          }
290        }, 300);
291      }
292    },
293
294    // Utility methods
295    formatNumber(num) {
296      if (!num) return "0";
297      if (num >= 1000000) {
298        return (num / 1000000).toFixed(1) + "M";
299      } else if (num >= 1000) {
300        return (num / 1000).toFixed(1) + "K";
301      }
302      return num.toString();
303    },
304
305    formatDate(dateString) {
306      if (!dateString) return "";
307      const date = new Date(dateString);
308      const now = new Date();
309      const diffTime = Math.abs(now - date);
310      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
311
312      if (diffDays < 1) {
313        return "Today";
314      } else if (diffDays < 7) {
315        return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`;
316      } else if (diffDays < 30) {
317        const weeks = Math.floor(diffDays / 7);
318        return `${weeks} week${weeks !== 1 ? "s" : ""} ago`;
319      } else if (diffDays < 365) {
320        const months = Math.floor(diffDays / 30);
321        return `${months} month${months !== 1 ? "s" : ""} ago`;
322      } else {
323        const years = Math.floor(diffDays / 365);
324        return `${years} year${years !== 1 ? "s" : ""} ago`;
325      }
326    },
327
328    formatDuration(duration) {
329      if (!duration) return "";
330      const match = duration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
331      if (!match) return duration;
332
333      const hours = parseInt(match[1] || 0);
334      const minutes = parseInt(match[2] || 0);
335      const seconds = parseInt(match[3] || 0);
336
337      if (hours > 0) {
338        return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
339      }
340      return `${minutes}:${seconds.toString().padStart(2, "0")}`;
341    },
342
343    // Handle video modal keyboard navigation
344    handleModalKeydown(event) {
345      // Tab navigation within modal
346      if (event.key === 'Tab') {
347        const focusableElements = document.querySelectorAll(
348          '.naperastro-video-modal button, .naperastro-video-modal [tabindex="0"]'
349        );
350        const firstElement = focusableElements[0];
351        const lastElement = focusableElements[focusableElements.length - 1];
352
353        if (event.shiftKey) {
354          if (document.activeElement === firstElement) {
355            event.preventDefault();
356            lastElement.focus();
357          }
358        } else {
359          if (document.activeElement === lastElement) {
360            event.preventDefault();
361            firstElement.focus();
362          }
363        }
364      }
365      
366      // 'i' key to toggle description
367      if (event.key === 'i' || event.key === 'I') {
368        event.preventDefault();
369        this.toggleDescription();
370      }
371    },
372
373    // Handle smooth image loading
374    handleImageLoad(event) {
375      event.target.classList.add("loaded");
376    },
377
378    handleImageError(event) {
379      event.target.style.backgroundColor = "#f0f0f0";
380      event.target.alt = "Image unavailable";
381    },
382
383    // Enhanced search functionality
384    focusSearch() {
385      this.searchFocused = true;
386    },
387
388    blurSearch() {
389      this.searchFocused = false;
390    },
391
392    clearSearch() {
393      this.searchQuery = "";
394      this.applyFilters();
395    },
396
397    // Keyboard shortcut handler
398    handleKeyboardShortcuts(event) {
399      // Ctrl/Cmd + K to focus search
400      if ((event.ctrlKey || event.metaKey) && event.key === "k") {
401        event.preventDefault();
402        const searchInput = document.querySelector(".naperastro-search-input");
403        if (searchInput) {
404          searchInput.focus();
405          searchInput.select();
406        }
407      }
408
409      // Escape to clear search when focused
410      if (event.key === "Escape" && this.searchFocused && this.searchQuery) {
411        this.clearSearch();
412      }
413    },
414
415    // Quick search suggestions (could be expanded)
416    getSearchSuggestions() {
417      if (!this.searchQuery || this.searchQuery.length < 2) return [];
418
419      const suggestions = new Set();
420      const query = this.searchQuery.toLowerCase();
421
422      this.allVideos.forEach((video) => {
423        // Extract words from title
424        const words = video.title.toLowerCase().split(/\s+/);
425        words.forEach((word) => {
426          if (word.startsWith(query) && word.length > query.length) {
427            suggestions.add(word);
428          }
429        });
430      });
431
432      return Array.from(suggestions).slice(0, 5);
433    },
434
435    // Check if a video is the introduction video
436    isIntroVideo(videoId) {
437      return videoId === this.introVideoId;
438    },
439
440    // Track video engagement analytics
441    trackVideoEngagement(videoId, action, source = 'grid') {
442      if (typeof gtag !== 'undefined') {
443        gtag('event', 'video_' + action, {
444          'video_id': videoId,
445          'event_category': 'video_engagement',
446          'event_label': source,
447          'is_intro_video': this.isIntroVideo(videoId)
448        });
449      }
450    },
451  };
452}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.