1/** 2 * Favorites Manager 3 * Handles saving and removing favorites for jobs and events 4 */ 5 6function initFavoritesManager() { 7 'use strict'; 8 9 // Global favorites manager 10 const favoritesManager = { 11 favorites: {}, 12 isInitialized: false, 13 14 /** 15 * Initialize the favorites manager 16 */ 17 init: function () { 18 if (this.isInitialized) return; 19 20 // Bind event listeners to favorite buttons 21 this.bindFavoriteButtons(); 22 23 // Load user's favorites if logged in 24 this.loadFavorites(); 25 26 this.isInitialized = true; 27 }, 28 29 /** 30 * Bind click events to favorite buttons 31 */ 32 bindFavoriteButtons: function () { 33 document.addEventListener('click', (e) => { 34 if ( 35 e.target.closest('.favorite-btn') || 36 e.target.closest('.dashboard-favorite-btn') 37 ) { 38 e.preventDefault(); 39 const button = 40 e.target.closest('.favorite-btn') || 41 e.target.closest('.dashboard-favorite-btn'); 42 this.handleFavoriteClick(button); 43 } 44 }); 45 }, 46 47 /** 48 * Handle favorite button click 49 */ 50 handleFavoriteClick: function (button) { 51 const itemId = button.dataset.id; 52 const itemType = button.dataset.type; 53 const itemTitle = button.dataset.title || ''; 54 const itemPermalink = button.dataset.permalink || ''; 55 56 if (!itemId || !itemType) { 57 return; 58 } 59 60 // Check if user is logged in 61 if (!this.isUserLoggedIn()) { 62 this.showMessage(daroganFavorites.strings.loginRequired, 'error'); 63 return; 64 } 65 66 // Check if already favorited 67 const itemKey = itemType + '_' + itemId; 68 const isFavorited = this.favorites[itemKey]; 69 70 if (isFavorited) { 71 this.removeFavorite(itemId, itemType, button); 72 } else { 73 this.saveFavorite(itemId, itemType, itemTitle, itemPermalink, button); 74 } 75 }, 76 77 /** 78 * Save a favorite 79 */ 80 saveFavorite: function ( 81 itemId, 82 itemType, 83 itemTitle, 84 itemPermalink, 85 button 86 ) { 87 const data = { 88 item_id: itemId, 89 item_type: itemType, 90 item_title: itemTitle, 91 item_permalink: itemPermalink, 92 }; 93 94 fetch(daroganFavorites.restUrl + 'favorites/save', { 95 method: 'POST', 96 headers: { 97 'Content-Type': 'application/json', 98 'X-WP-Nonce': daroganFavorites.nonce, 99 }, 100 body: JSON.stringify(data), 101 }) 102 .then((response) => { 103 return response.text().then((text) => { 104 try { 105 return JSON.parse(text); 106 } catch (e) { 107 throw new Error( 108 'Invalid JSON response: ' + text.substring(0, 200) 109 ); 110 } 111 }); 112 }) 113 .then((data) => { 114 if (data.success) { 115 // Add to local favorites 116 const itemKey = itemType + '_' + itemId; 117 this.favorites[itemKey] = { 118 id: itemId, 119 type: itemType, 120 title: itemTitle, 121 permalink: itemPermalink, 122 date_added: new Date().toISOString(), 123 }; 124 125 // Update button state 126 this.updateButtonState(button, true); 127 128 // Update dashboard if on dashboard page 129 this.updateDashboard(); 130 } else { 131 this.showMessage( 132 data.data || daroganFavorites.strings.error, 133 'error' 134 ); 135 } 136 }) 137 .catch((error) => { 138 this.showMessage(daroganFavorites.strings.error, 'error'); 139 }); 140 }, 141 142 /** 143 * Remove a favorite 144 */ 145 removeFavorite: function (itemId, itemType, button) { 146 const data = { 147 item_id: itemId, 148 item_type: itemType, 149 }; 150 151 fetch(daroganFavorites.restUrl + 'favorites/remove', { 152 method: 'POST', 153 headers: { 154 'Content-Type': 'application/json', 155 'X-WP-Nonce': daroganFavorites.nonce, 156 }, 157 body: JSON.stringify(data), 158 }) 159 .then((response) => { 160 return response.text().then((text) => { 161 try { 162 return JSON.parse(text); 163 } catch (e) { 164 throw new Error( 165 'Invalid JSON response: ' + text.substring(0, 200) 166 ); 167 } 168 }); 169 }) 170 .then((data) => { 171 if (data.success) { 172 // Remove from local favorites 173 const itemKey = itemType + '_' + itemId; 174 delete this.favorites[itemKey]; 175 176 // Update button state 177 this.updateButtonState(button, false); 178 179 // Update dashboard if on dashboard page 180 this.updateDashboard(); 181 } else { 182 this.showMessage( 183 data.data || daroganFavorites.strings.error, 184 'error' 185 ); 186 } 187 }) 188 .catch((error) => { 189 console.error('Error removing favorite:', error); 190 this.showMessage(daroganFavorites.strings.error, 'error'); 191 }); 192 }, 193 194 /** 195 * Load user's favorites 196 */ 197 loadFavorites: function () { 198 if (!this.isUserLoggedIn()) return; 199 200 fetch(daroganFavorites.restUrl + 'favorites/get', { 201 method: 'GET', 202 headers: { 203 'X-WP-Nonce': daroganFavorites.nonce, 204 }, 205 }) 206 .then((response) => { 207 return response.text().then((text) => { 208 try { 209 return JSON.parse(text); 210 } catch (e) { 211 console.error('Failed to parse JSON:', e); 212 throw new Error( 213 'Invalid JSON response: ' + text.substring(0, 200) 214 ); 215 } 216 }); 217 }) 218 .then((data) => { 219 if (data.success) { 220 this.favorites = data.favorites || {}; 221 this.updateAllButtonStates(); 222 223 // Trigger dashboard update after favorites are loaded 224 if (document.getElementById('saved-jobs-container')) { 225 this.updateDashboard(); 226 } 227 } else { 228 console.error('AJAX request failed:', data); 229 } 230 }) 231 .catch((error) => { 232 console.error('Error loading favorites:', error); 233 }); 234 }, 235 236 /** 237 * Update all favorite button states 238 */ 239 updateAllButtonStates: function () { 240 const buttons = document.querySelectorAll( 241 '.favorite-btn, .dashboard-favorite-btn' 242 ); 243 buttons.forEach((button) => { 244 const itemId = button.dataset.id; 245 const itemType = button.dataset.type; 246 247 if (itemId && itemType) { 248 const itemKey = itemType + '_' + itemId; 249 const isFavorited = this.favorites[itemKey]; 250 this.updateButtonState(button, isFavorited); 251 } 252 }); 253 }, 254 255 /** 256 * Update button state 257 */ 258 updateButtonState: function (button, isFavorited) { 259 const icon = button.querySelector('i'); 260 const text = button.querySelector('.favorite-text'); 261 const isDashboardButton = button.classList.contains( 262 'dashboard-favorite-btn' 263 ); 264 265 if (isFavorited) { 266 button.classList.add('favorited'); 267 if (icon) { 268 icon.className = isDashboardButton 269 ? 'fa-solid fa-heart' 270 : 'fa-solid fa-heart me-1'; 271 } 272 if (text && !isDashboardButton) { 273 text.textContent = daroganFavorites.strings.savedToFavourites; 274 } 275 } else { 276 button.classList.remove('favorited'); 277 if (icon) { 278 icon.className = isDashboardButton 279 ? 'fa-regular fa-heart' 280 : 'fa-regular fa-heart me-1'; 281 } 282 if (text && !isDashboardButton) { 283 text.textContent = daroganFavorites.strings.saveToFavourites; 284 } 285 } 286 }, 287 288 /** 289 * Update dashboard with favorites 290 */ 291 updateDashboard: function () { 292 const savedJobsContainer = document.getElementById(
293 'saved-jobs-container' 294 ); 295 if (!savedJobsContainer) return; 296 297 // Filter job favorites 298 const jobFavorites = Object.values(this.favorites).filter( 299 (fav) => fav.type === 'job' 300 ); 301 302 if (jobFavorites.length === 0) { 303 // Show empty state 304 savedJobsContainer.innerHTML = ` 305 <div class="empty-state text-center py-5"> 306 <div class="row"> 307 <div class="col-8 offset-2 col-sm-6 offset-sm-3 col-lg-4 offset-lg-4 text-center mb-4"> 308 <img src="${daroganFavorites.themeUrl}/resources/images/sticker-marilwyd.svg" alt="" class="img-fluid" style="max-width: 200px;" /> 309 </div> 310 <div class="col-12 col-lg-8 offset-lg-2 text-center"> 311 <h3 class="mb-3">${daroganFavorites.strings.getStarted}</h3> 312 <p class="mb-4">${daroganFavorites.strings.noJobsSaved}</p> 313 <a href="/jobs" class="btn btn-primary">${daroganFavorites.strings.exploreJobs}</a> 314 </div> 315 </div> 316 </div> 317 `; 318 } else { 319 // Show loading state 320 savedJobsContainer.innerHTML = ` 321 <div class="text-center py-4"> 322 <div class="spinner-border" role="status"> 323 <span class="visually-hidden">Loading...</span> 324 </div> 325 <p class="mt-2">${daroganFavorites.strings.loadingJobs}</p> 326 </div> 327 `; 328 329 // Fetch full job details 330 this.fetchJobDetails(jobFavorites, savedJobsContainer); 331 } 332 }, 333 334 /** 335 * Fetch full job details for saved jobs 336 */ 337 fetchJobDetails: function (jobFavorites, container) { 338 if (window.jobManager) { 339 window.jobManager.fetchJobDetails(jobFavorites, container); 340 } 341 }, 342 343 /** 344 * Check if user is logged in 345 */ 346 isUserLoggedIn: function () { 347 // Check if we're on a page with user data 348 const isLoggedIn = document.body.classList.contains('logged-in'); 349 return isLoggedIn; 350 }, 351 352 /** 353 * Show message to user 354 */ 355 showMessage: function (message, type) { 356 // Create or find existing message container 357 let messageContainer = document.getElementById('favorites-message'); 358 if (!messageContainer) { 359 messageContainer = document.createElement('div'); 360 messageContainer.id = 'favorites-message'; 361 messageContainer.style.cssText = ` 362 position: fixed; 363 top: 20px; 364 right: 20px; 365 z-index: 9999; 366 padding: 15px 20px; 367 border-radius: 8px; 368 color: white; 369 font-weight: 600; 370 font-size: 14px; 371 max-width: 350px; 372 word-wrap: break-word; 373 line-height: 1.4; 374 transition: all 0.3s ease; 375 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 376 border: 2px solid; 377 opacity: 0; 378 transform: translateX(100%); 379 display: flex; 380 align-items: center; 381 gap: 8px; 382 `; 383 document.body.appendChild(messageContainer); 384 } 385 386 // Set message content and style based on type 387 const icon = type === 'success' ? 'â' : 'â'; 388 messageContainer.innerHTML = `<span style="font-size: 16px;">${icon}</span><span>${message}</span>`; 389 390 if (type === 'success') { 391 messageContainer.style.backgroundColor = 'var(--darogan-red)'; 392 messageContainer.style.borderColor = 'var(--darogan-red)'; 393 messageContainer.style.color = 'white'; 394 } else { 395 messageContainer.style.backgroundColor = '#dc3545'; 396 messageContainer.style.borderColor = '#dc3545'; 397 messageContainer.style.color = 'white'; 398 } 399 400 // Show message with animation 401 messageContainer.style.opacity = '1'; 402 messageContainer.style.transform = 'translateX(0)'; 403 404 // Hide message after 3 seconds with animation 405 setTimeout(() => { 406 messageContainer.style.opacity = '0'; 407 messageContainer.style.transform = 'translateX(100%)'; 408 }, 3000); 409 }, 410 }; 411 412 // Make it available globally 413 window.favoritesManager = favoritesManager; 414 415 // Initialize when DOM is ready 416 if (document.readyState === 'loading') { 417 document.addEventListener('DOMContentLoaded', () => { 418 favoritesManager.init(); 419 }); 420 } else { 421 favoritesManager.init(); 422 } 423 424 return favoritesManager; 425} 426 427// Make it available globally for WordPress enqueue 428window.initFavoritesManager = initFavoritesManager;
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.