1/** 2 * Cloudstay Wishlist/Favorites 3 * 4 * Client-side wishlist functionality using localStorage 5 * 6 * @package Cloudstay 7 * @since 1.3.0 8 */ 9 10(function() { 11 'use strict'; 12 13 const STORAGE_KEY = 'cloudstay_wishlist'; 14 15 /** 16 * Wishlist manager 17 */ 18 const wishlist = { 19 items: [], 20 21 /** 22 * Initialize wishlist from localStorage 23 */ 24 init() { 25 try { 26 const stored = localStorage.getItem(STORAGE_KEY); 27 this.items = stored ? JSON.parse(stored) : []; 28 } catch (e) { 29 console.error('Error loading wishlist:', e); 30 this.items = []; 31 } 32 this.updateAllButtons(); 33 this.updateWishlistCount(); 34 }, 35 36 /** 37 * Save wishlist to localStorage 38 */ 39 save() { 40 try { 41 localStorage.setItem(STORAGE_KEY, JSON.stringify(this.items)); 42 } catch (e) { 43 console.error('Error saving wishlist:', e); 44 } 45 }, 46 47 /** 48 * Add item to wishlist 49 */ 50 add(id, data = {}) { 51 // Convert to string for consistent comparison 52 id = String(id); 53 if (this.has(id)) return false; 54 55 this.items.push({ 56 id: id, 57 name: data.name || '', 58 image: data.image || '', 59 price: data.price || 0, 60 currency: data.currency || 'USD', 61 symbol: data.symbol || '$', 62 location: data.location || '', 63 url: data.url || '', 64 bedrooms: data.bedrooms || 0, 65 bathrooms: data.bathrooms || 0, 66 rating: data.rating || 0, 67 addedAt: new Date().toISOString() 68 }); 69 70 this.save(); 71 this.updateAllButtons(); 72 this.updateWishlistCount(); 73 this.showNotification('Added to wishlist'); 74 return true; 75 }, 76 77 /** 78 * Remove item from wishlist 79 */ 80 remove(id) { 81 id = String(id); 82 const index = this.items.findIndex(item => String(item.id) === id); 83 if (index === -1) return false; 84 85 this.items.splice(index, 1); 86 this.save(); 87 this.updateAllButtons(); 88 this.updateWishlistCount(); 89 this.showNotification('Removed from wishlist'); 90 return true; 91 }, 92 93 /** 94 * Toggle item in wishlist 95 */ 96 toggle(id, data = {}) { 97 id = String(id); 98 if (this.has(id)) { 99 return this.remove(id); 100 } else { 101 return this.add(id, data); 102 } 103 }, 104 105 /** 106 * Check if item is in wishlist 107 */ 108 has(id) { 109 id = String(id); 110 return this.items.some(item => String(item.id) === id); 111 }, 112 113 /** 114 * Get all wishlist items 115 */ 116 getAll() { 117 return [...this.items]; 118 }, 119 120 /** 121 * Clear all wishlist items 122 */ 123 clear() { 124 this.items = []; 125 this.save(); 126 this.updateAllButtons(); 127 this.updateWishlistCount(); 128 }, 129 130 /** 131 * Get wishlist count 132 */ 133 count() { 134 return this.items.length; 135 }, 136 137 /** 138 * Update all wishlist button states on page 139 */ 140 updateAllButtons() { 141 document.querySelectorAll('[data-wishlist-id]').forEach(btn => { 142 const id = btn.dataset.wishlistId; 143 this.updateButtonState(btn, this.has(id)); 144 }); 145 }, 146 147 /** 148 * Update single button state 149 */ 150 updateButtonState(btn, isActive) { 151 if (isActive) { 152 btn.classList.add('active'); 153 btn.setAttribute('aria-label', 'Remove from wishlist'); 154 } else { 155 btn.classList.remove('active'); 156 btn.setAttribute('aria-label', 'Add to wishlist'); 157 } 158 159 // Update heart icon if present 160 const svg = btn.querySelector('svg'); 161 if (svg) { 162 svg.setAttribute('fill', isActive ? '#ef4444' : 'none'); 163 } 164 }, 165 166 /** 167 * Update wishlist count badge in header 168 */ 169 updateWishlistCount() { 170 const countElements = document.querySelectorAll('.wishlist-count'); 171 const count = this.count(); 172
173 countElements.forEach(el => { 174 el.textContent = count; 175 el.style.display = count > 0 ? 'inline-flex' : 'none'; 176 }); 177 }, 178 179 /** 180 * Show notification 181 */ 182 showNotification(message) { 183 // Remove existing notification 184 const existing = document.querySelector('.wishlist-notification'); 185 if (existing) { 186 existing.remove(); 187 } 188 189 // Create notification 190 const notification = document.createElement('div'); 191 notification.className = 'wishlist-notification'; 192 notification.innerHTML = ` 193 <svg viewBox="0 0 24 24" fill="#ef4444" width="20" height="20"> 194 <path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/> 195 </svg> 196 <span>${message}</span> 197 `; 198 199 document.body.appendChild(notification); 200 201 // Animate in 202 requestAnimationFrame(() => { 203 notification.classList.add('show'); 204 }); 205 206 // Remove after delay 207 setTimeout(() => { 208 notification.classList.remove('show'); 209 setTimeout(() => notification.remove(), 300); 210 }, 2000); 211 } 212 }; 213 214 /** 215 * Bind wishlist button events 216 */ 217 function bindWishlistButtons() { 218 document.addEventListener('click', (e) => { 219 const btn = e.target.closest('[data-wishlist-id]'); 220 if (!btn) return; 221 222 e.preventDefault(); 223 e.stopPropagation(); 224 225 const id = btn.dataset.wishlistId; 226 const data = { 227 name: btn.dataset.wishlistName || '', 228 image: btn.dataset.wishlistImage || '', 229 price: parseFloat(btn.dataset.wishlistPrice) || 0, 230 currency: btn.dataset.wishlistCurrency || 'USD', 231 symbol: btn.dataset.wishlistSymbol || '$', 232 location: btn.dataset.wishlistLocation || '', 233 url: btn.dataset.wishlistUrl || window.location.href, 234 bedrooms: parseInt(btn.dataset.wishlistBedrooms) || 0, 235 bathrooms: parseFloat(btn.dataset.wishlistBathrooms) || 0, 236 rating: parseFloat(btn.dataset.wishlistRating) || 0 237 }; 238 239 wishlist.toggle(id, data); 240 }); 241 } 242 243 /** 244 * Initialize when DOM is ready 245 */ 246 function init() {
247 wishlist.init(); 248 bindWishlistButtons(); 249 } 250 251 // Expose wishlist globally 252 window.cloudstayWishlist = wishlist; 253 254 // Initialize 255 if (document.readyState === 'loading') { 256 document.addEventListener('DOMContentLoaded', init); 257 } else { 258 init(); 259 } 260 261})();
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.