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.