1// main-tools-script.js - Fixed Links & Buttons (Stable Version) 2 3document.addEventListener('DOMContentLoaded', () => { 4 // --- Language and Data Initialization --- 5 const currentLang = document.documentElement.lang || 'en'; 6 const langData = (typeof AllData !== 'undefined' && AllData[currentLang]) ? AllData[currentLang] : (typeof AllData !== 'undefined' ? AllData['en'] : { 7 UIStrings: {}, 8 ToolsData: { 9 tools: {}, 10 breakingNews: [] 11 } 12 }); 13 const UIStrings = langData.UIStrings; 14 const ToolsData = langData.ToolsData; 15 16 // --- UTILITY AND SECURITY OBJECTS --- 17 const PerformanceUtils = { 18 debounce(func, wait) { 19 let timeout; 20 return function executedFunction(...args) { 21 const later = () => { 22 clearTimeout(timeout); 23 func(...args); 24 }; 25 clearTimeout(timeout); 26 timeout = setTimeout(later, wait); 27 }; 28 }, 29 throttle(func, limit) { 30 let inThrottle; 31 return function () { 32 const args = arguments; 33 const context = this; 34 if (!inThrottle) { 35 func.apply(context, args); 36 inThrottle = true; 37 setTimeout(() => inThrottle = false, limit); 38 } 39 } 40 }, 41 batchDOMUpdates(callback) { 42 requestAnimationFrame(callback); 43 }, 44 createFragment() { 45 return document.createDocumentFragment(); 46 } 47 }; 48 const InputSecurity = { 49 cache: new Map(), 50 sanitizeHTML(input) { 51 if (this.cache.has(input)) return this.cache.get(input); 52 const div = document.createElement('div'); 53 div.textContent = input; 54 const result = div.innerHTML; 55 if (this.cache.size > 100) this.cache.clear(); 56 this.cache.set(input, result); 57 return result; 58 }, 59 validateEmail(email) { 60 const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 61 return emailRegex.test(email) && email.length <= 254; 62 }, 63 sanitizeSearchInput(input) { 64 return input.replace(/[<>\"'&]/g, '').replace(/javascript:/gi, '').replace(/on\w+=/gi, '').substring(0, 100); 65 }, 66 validateFormInput(input, type) { 67 const sanitized = this.sanitizeHTML(input.trim()); 68 switch (type) { 69 case 'email': 70 return this.validateEmail(sanitized) ? sanitized : null; 71 case 'name': 72 return sanitized.length > 0 && sanitized.length <= 50 ? sanitized : null; 73 case 'message': 74 return sanitized.length > 0 && sanitized.length <= 1000 ? sanitized : null; 75 case 'search': 76 return this.sanitizeSearchInput(sanitized); 77 default: 78 return sanitized; 79 } 80 } 81 }; 82 const RateLimit = { 83 storage: new Map(), 84 cleanOldEntries() { 85 const now = Date.now(); 86 for (const [key, data] of this.storage.entries()) { 87 if (now > data.resetTime) { 88 this.storage.delete(key); 89 } 90 } 91 }, 92 isAllowed(action, identifier = 'global') { 93 if (this.storage.size > 50) this.cleanOldEntries(); 94 const key = `${action}_${identifier}`; 95 const now = Date.now(); 96 const config = (typeof SecurityConfig !== 'undefined' && SecurityConfig.rateLimits[action]) ? SecurityConfig.rateLimits[action] : { 97 attempts: 50, 98 window: 60000 99 }; 100 if (!config) return true; 101 if (!this.storage.has(key)) { 102 this.storage.set(key, { 103 count: 1, 104 resetTime: now + config.window 105 }); 106 return true; 107 } 108 const data = this.storage.get(key); 109 if (now > data.resetTime) { 110 this.storage.set(key, { 111 count: 1, 112 resetTime: now + config.window 113 }); 114 return true; 115 } 116 if (data.count >= config.attempts) return false;
117 data.count++; 118 return true; 119 } 120 }; 121 const SecureStorage = { 122 prefix: 'aitools_', 123 setItem(key, value) { 124 try { 125 localStorage.setItem(this.prefix + key, JSON.stringify(value)); 126 } catch (e) { 127 console.warn('Storage limit exceeded'); 128 } 129 }, 130 getItem(key) { 131 try { 132 const item = localStorage.getItem(this.prefix + key); 133 return item ? JSON.parse(item) : null; 134 } catch (e) { 135 return null; 136 } 137 }, 138 removeItem(key) { 139 try { 140 localStorage.removeItem(this.prefix + key); 141 } catch (e) { 142 console.warn("Could not remove item from storage."); 143 } 144 } 145 }; 146 147 const loadMoreArrowSVG = `<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>`; 148 149 function getAllTools() { 150 if (getAllTools.cache) return getAllTools.cache; 151 let allTools = []; 152 for (const category in ToolsData.tools) { 153 allTools = allTools.concat(ToolsData.tools[category]); 154 } 155 getAllTools.cache = allTools; 156 return allTools; 157 } 158 159 function getToolsByCategory(category) { 160 if (category === 'all') return getAllTools(); 161 if (category in ToolsData.tools) return ToolsData.tools[category]; 162 return getAllTools().filter(tool => tool.categories.includes(category)); 163 } 164 165 function getToolsByTab(tab) { 166 if (tab === 'favorites') return getFavoriteTools(); 167 if (tab === 'all') return getAllTools(); 168 return getAllTools().filter(tool => tool.tab && tool.tab.includes(tab)); 169 } 170 171 function getFavoriteTools() { 172 const favorites = getFavorites(); 173 return getAllTools().filter(tool => favorites.includes(tool.id)); 174 } 175 176 function getCategoryDisplayName(category) { 177 // (Mapping kept as original) 178 const enMap = { 179 'text': 'Writing', 180 'ai': 'AI', 181 'image': 'Images', 182 'art': 'Art', 183 'design': 'Design', 184 'code': 'Coding', 185 'development': 'Development', 186 'video': 'Video', 187 'content': 'Content', 188 'audio': 'Audio', 189 'docs': 'Documents', 190 'marketing': 'Marketing', 191 'creation': 'Content Creation', 192 'productivity': 'Productivity', 193 'voice': 'Voice', 194 'ocr': 'OCR', 195 'data': 'Data', 196 'writing': 'Writing', 197 'translation': 'Translation', 198 'gaming': 'Gaming', 199 'ecommerce': 'eCommerce', 200 'editing': 'Editing', 201 'seo': 'SEO', 202 'ide': 'IDE', 203 'web': 'Web', 204 'business': 'Business', 205 'social': 'Social Media', 206 'streaming': 'Streaming', 207 'podcast': 'Podcast', 208 'music': 'Music', 209 'research': 'Research', 210 'education': 'Education', 211 'testing': 'Testing', 212 'ui/ux': 'UI/UX', 213 '3d': '3D' 214 }; 215 const arMap = { 216 'text': 'ÙØªØ§Ø¨Ø©', 217 'ai': 'AI', 218 'image': 'ØµÙØ±', 219 'art': 'ÙÙ', 220 'design': 'تص٠ÙÙ ', 221 'code': 'بر٠جة', 222 'development': 'تطÙÙØ±', 223 'video': 'ÙÙØ¯ÙÙ', 224 'content': 'Ù ØØªÙÙ', 225 'audio': 'ØµÙØª', 226 'docs': 'Ù Ø³ØªÙØ¯Ø§Øª', 227 'marketing': 'تسÙÙÙ', 228 'creation': 'إبداع', 229 'productivity': 'Ø¥ÙØªØ§Ø¬ÙØ©', 230 'voice': 'ØµÙØª', 231 'ocr': 'OCR', 232 'data': 'Ø¨ÙØ§Ùات', 233 'writing': 'ÙØªØ§Ø¨Ø©', 234 'translation': 'ترج٠ة', 235 'gaming': 'Ø£ÙØ¹Ø§Ø¨', 236 'ecommerce': 'تجارة Ø¥ÙÙØªØ±ÙÙÙØ©', 237 'editing': 'ØªØØ±Ùر', 238 'seo': 'SEO', 239 'ide': 'IDE', 240 'web': 'ÙÙØ¨', 241 'business': 'أع٠اÙ', 242 'social': 'Ø³ÙØ´ÙØ§Ù Ù ÙØ¯Ùا', 243 'streaming': 'بث', 244 'podcast': 'Ø¨ÙØ¯Ùاست', 245 'music': 'Ù ÙØ³ÙÙÙ', 246 'research': 'Ø¨ØØ«', 247 'education': 'تعÙÙÙ ', 248 'testing': 'اختبار', 249 'ui/ux': 'UI/UX', 250 '3d': '3D' 251 }; 252 const map = currentLang === 'ar' ? arMap : enMap; 253 return map[category] || category.charAt(0).toUpperCase() + category.slice(1); 254 } 255 256 function createToolCard(tool, delay = 0) { 257 const subscriptions = tool.subscription.split(','); 258 const categories = tool.categories.split(','); 259 const favorites = getFavorites(); 260 const isFavorite = favorites.includes(tool.id); 261 262 const article = document.createElement('article'); 263 article.className = 'tool-card fade-in'; 264 Object.assign(article.dataset, { 265 toolId: tool.id, 266 delay: delay 267 }); 268 269 const cardContent = document.createElement('div'); 270 cardContent.className = 'tool-card-content p-5 flex flex-col h-full'; 271 272 // --- [START] Header: Circular Logo + Title + Favorite --- 273 const header = document.createElement('header'); 274 header.className = 'flex items-center gap-4 mb-5 relative'; 275 276 // 1. ØØ§ÙÙØ© اÙÙÙØ¬Ù Ø§ÙØ¯Ø§Ø¦Ø±Ù 277 const logoContainer = document.createElement('div'); 278 logoContainer.className = 'w-14 h-14 flex-shrink-0 rounded-full overflow-hidden border-2 border-white dark:border-gray-700 shadow-md flex items-center justify-center bg-primary/10'; 279 280 const logoImg = document.createElement('img'); 281 logoImg.className = 'w-full h-full object-cover'; // ÙØ¶Ù Ø§Ù Ù ÙØ¡ Ø§ÙØ¯Ø§Ø¦Ø±Ø© بشÙ٠٠ثاÙÙ 282 logoImg.alt = tool.name; 283 284 // Ù ÙØ·Ù Ø§Ø®ØªÙØ§Ø± Ø§ÙØµÙرة: ÙØ¯ÙÙØ© Ø£Ù ØØ±ÙÙ 285 if (tool.logo && tool.logo.trim() !== "") { 286 // إذا Ø£Ø¶ÙØª ٠سار ØµÙØ±Ø© ÙÙ Ù ÙÙ Ø§ÙØ¨ÙØ§ÙØ§Øª ٠ث٠"img/atoms.png" 287 logoImg.src = tool.logo; 288 } else { 289 // إذا ÙÙ ØªØ¶Ù ØµÙØ±Ø©Ø ÙØªÙ Ø¥ÙØ´Ø§Ø¡ دائرة Ù ÙÙÙØ© بداخÙÙØ§ Ø£ÙÙ ØØ±ÙÙ Ø§ÙØ§Ø³Ù 290 // ÙØ³ØªØ®Ø¯Ù خد٠ة ui-avatars ÙØªÙÙÙØ¯ Ø§ÙØØ±Ù٠بشÙÙ Ø¯Ø§Ø¦Ø±Ù ÙØ§ØØªØ±Ø§ÙÙ
291 logoImg.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(tool.name)}&background=7e22ce&color=fff&bold=true&rounded=true&size=128`; 292 } 293 294 // ÙÙ ØØ§ÙØ© ØØ¯ÙØ« خطأ Ù٠تØÙ ÙÙ Ø§ÙØµÙرة اÙÙØ¯ÙÙØ©Ø ÙØªÙ Ø§ÙØ±Ø¬Ùع ÙÙØØ±ÙÙ 295 logoImg.onerror = function () { 296 this.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(tool.name)}&background=7e22ce&color=fff&bold=true&rounded=true`; 297 }; 298 299 logoContainer.appendChild(logoImg); 300 301 // 2. ØØ§ÙÙØ© Ø§ÙØ¹ÙÙØ§Ù 302 const titleAndBadges = document.createElement('div'); 303 titleAndBadges.className = 'flex-grow min-w-0 pr-10'; // Ù Ø³Ø§ØØ© ÙØ²Ø± اÙÙÙØ¨ 304 305 const title = document.createElement('h3'); 306 title.className = 'tool-card-title text-xl font-bold truncate text-gray-800 dark:text-white'; 307 title.textContent = tool.name; 308 titleAndBadges.appendChild(title); 309 310 // 3. زر اÙÙ ÙØ¶ÙØ© (ÙØ¨ÙÙ ÙÙ Ù ÙØ§ÙÙ Ø¨Ø§ÙØ£Ø¹ÙÙ) 311 const favButton = document.createElement('button'); 312 favButton.className = `favorites-badge absolute top-0 right-0 ${isFavorite ? 'active' : ''}`; 313 favButton.dataset.toolId = tool.id; 314 const favIcon = document.createElement('i'); 315 favIcon.className = `fa-${isFavorite ? 'solid' : 'regular'} fa-heart`; 316 favButton.appendChild(favIcon); 317 318 header.append(logoContainer, titleAndBadges, favButton); 319 // --- [END] Header --- 320 321 // ÙØµÙ Ø§ÙØ£Ø¯Ø§Ø© 322 const description = document.createElement('p'); 323 description.className = 'tool-card-description text-gray-500 dark:text-gray-400 text-sm mb-6 line-clamp-3 leading-relaxed'; 324 description.textContent = tool.description; 325 326 // Ø§ÙØªØµÙÙÙØ§Øª ÙØ§Ùأسعار 327 // Ø§ÙØªØµÙÙÙØ§Øª ÙØ§Ùأسعار (ØªÙØ³ÙÙ Ù ØØ³Ù) 328 const tagsContainer = document.createElement('div'); 329 tagsContainer.className = 'flex flex-wrap items-center justify-between gap-3 mt-auto mb-6'; 330 331 // 1. ØØ§ÙÙØ© Ø§ÙØ³Ø¹Ø± (FREEMIUM / PAID / FREE) 332 const pricingDiv = document.createElement('div'); 333 pricingDiv.className = 'flex gap-2'; 334 subscriptions.forEach(sub => { 335 const span = document.createElement('span'); 336 // أضÙÙØ§ shadow Ø®ÙÙ٠٠جعÙÙØ§ Ø§ÙØ®Ø· أغ٠٠ÙÙÙÙØ§Ù ÙÙÙØ¶ÙØ 337 span.className = `pricing-tag px-3 py-1 rounded-full text-[10px] font-black uppercase shadow-sm tag-${sub.trim()}`; 338 span.textContent = UIStrings[sub.trim()] || sub.trim(); 339 pricingDiv.appendChild(span); 340 }); 341 342 // 2. ØØ§ÙÙØ© Ø§ÙØªØµÙÙÙ (Coding / AI / Ø¥ÙØ®) Ø¨Ø¬Ø§ÙØ¨Ùا ÙÙ Badge 343 const categoryDiv = document.createElement('div'); 344 categoryDiv.className = 'flex gap-2'; 345 categories.slice(0, 1).forEach(cat => { 346 const span = document.createElement('span'); 347 // جعÙÙØ§ Ø§ÙØªØµÙÙÙ ÙØ¨Ø¯Ù ÙÙ Badge Ø§ØØªØ±Ø§Ù٠بخÙÙÙØ© Ø®ÙÙÙØ© 348 span.className = 'text-[10px] font-bold px-3 py-1 bg-primary/20 text-primary-light border border-primary/30 rounded-full dark:text-purple-300'; 349 span.textContent = getCategoryDisplayName(cat.trim()); 350 categoryDiv.appendChild(span); 351 }); 352 353 tagsContainer.append(pricingDiv, categoryDiv); 354 355 tagsContainer.append(pricingDiv, categoryDiv); 356 357 // --- [START] ÙØ³Ù Ø§ÙØ£Ø²Ø±Ø§Ø± اÙÙ Ø·ÙØ± (Ø²ÙØ§Ø±Ø© + Ù ÙØ§Ù) --- 358 const footer = document.createElement('footer'); 359 footer.className = 'tool-card-actions mt-auto flex gap-2'; // جعÙÙØ§ Ø§ÙØØ§ÙÙØ© Ù Ø±ÙØ© ÙØªØ³ØªÙعب زرÙÙ 360 361 // 1. زر Ø²ÙØ§Ø±Ø© اÙÙ ÙÙØ¹ (Ø¯Ø§Ø¦Ù Ø§Ù Ù ÙØ¬Ùد) 362 const visitLink = document.createElement('a'); 363 visitLink.className = 'tool-action-btn btn-primary flex-grow flex items-center justify-center gap-2 py-3 rounded-xl font-bold transition-all text-sm active:scale-95'; 364 visitLink.href = tool.details?.website || '#'; 365 visitLink.target = '_blank'; 366 visitLink.rel = 'noopener noreferrer'; 367 visitLink.innerHTML = `<span>${UIStrings.visitWebsite}</span> <i class="fas fa-external-link-alt text-[10px]"></i>`; 368 369 // 2. زر اÙÙ ÙØ§Ù (ÙØ¸Ùر ÙÙØ· إذا Ø£Ø¶ÙØª سطر article ÙÙ Ø¨ÙØ§Ùات Ø§ÙØ£Ø¯Ø§Ø©) 370 if (tool.article && tool.article.trim() !== "") { 371 const articleLink = document.createElement('a'); 372 // ستاÙ٠ثاÙÙÙ (Secondary) ÙÙÙÙ٠٠ختÙÙØ§Ù ع٠زر Ø§ÙØ²Ùارة 373 articleLink.className = 'tool-action-btn btn-secondary flex-grow flex items-center justify-center gap-2 py-3 rounded-xl font-bold transition-all text-sm border border-primary/20 bg-primary/5 text-primary dark:text-purple-300 hover:bg-primary/10 active:scale-95'; 374 articleLink.href = tool.article; 375 // ÙÙ ÙÙ٠تغÙÙØ± Ø§ÙØ£ÙÙÙÙØ© Ù٠ا ÙÙØ§Ø³Ø¨Ù 376 articleLink.innerHTML = `<i class="fas fa-book-open text-[10px]"></i> <span>${UIStrings.readArticle}</span>`;
377 378 // ØªØ±ØªÙØ¨ Ø§ÙØ£Ø²Ø±Ø§Ø±: اÙÙ ÙØ§Ù عÙ٠اÙÙØ³Ø§Ø± ÙØ§ÙØ²ÙØ§Ø±Ø© عÙ٠اÙÙÙ ÙÙ (Ø£Ù Ø§ÙØ¹Ùس Ù٠ا ØªØØ¨) 379 footer.append(articleLink, visitLink); 380 } else { 381 // إذا ÙÙ ÙÙØ¬Ø¯ Ù ÙØ§ÙØ Ø§ÙØ²Ø± اÙÙØÙØ¯ ÙØ£Ø®Ø° Ø§ÙØ¹Ø±Ø¶ اÙÙØ§Ù Ù 382 visitLink.classList.add('w-full'); 383 footer.append(visitLink); 384 } 385 386 cardContent.append(header, description, tagsContainer, footer); 387 article.appendChild(cardContent); 388 return article; 389 } // ÙÙØ§ÙØ© Ø§ÙØ¯Ø§ÙØ© ÙÙØ§ 390 function showNotification(message, type = 'success') { 391 const container = document.getElementById('notification-container'); 392 if (!container) return; 393 const bgColor = type === 'success' ? 'bg-secondary' : 'bg-red-500'; 394 PerformanceUtils.batchDOMUpdates(() => { 395 const notification = document.createElement('div'); 396 notification.className = `notification ${bgColor} text-white px-4 py-2 rounded-lg shadow-lg mb-2`; 397 notification.textContent = InputSecurity.sanitizeHTML(message); 398 notification.setAttribute('role', 'alert'); 399 container.appendChild(notification); 400 setTimeout(() => { 401 notification.remove(); 402 }, 3300); 403 }); 404 } 405 406 function openModal(modal) { 407 if (!modal || modal.classList.contains('active')) return; 408 document.body.classList.add('modal-open'); 409 modal.classList.add('active'); 410 } 411 412 function closeModal(modal) { 413 if (!modal || !modal.classList.contains('active')) return; 414 modal.classList.remove('active'); 415 if (!document.querySelector('.modal.active, #favorites-modal.active, #newsletter-modal-overlay.active')) { 416 document.body.classList.remove('modal-open'); 417 } 418 } 419 420 function displayFavorites() { 421 const favoritesList = document.getElementById('favorites-list'); 422 if (!favoritesList) return; 423 const favoriteToolIds = getFavorites(); 424 const favoriteTools = getAllTools().filter(tool => favoriteToolIds.includes(tool.id)); 425 if (favoriteTools.length === 0) { 426 favoritesList.innerHTML = `<div class="text-center py-12 text-gray-400"><i class="fas fa-heart-broken text-5xl mb-4 text-gray-500"></i><p class="font-semibold text-lg">${UIStrings.noFavorites}</p><p class="text-sm mt-2">${UIStrings.noFavoritesHint}</p></div>`; 427 return; 428 } 429 const categoryIcons = { 430 'image': 'fas fa-image', 431 'text': 'fas fa-edit', 432 'code': 'fas fa-code', 433 'video': 'fas fa-video-camera', 434 'audio': 'fas fa-microphone-alt', 435 'docs': 'fas fa-file-alt', 436 'ai': 'fas fa-robot', 437 'art': 'fas fa-palette', 438 'design': 'fas fa-drafting-compass', 439 'development': 'fas fa-laptop-code', 440 'content': 'fas fa-newspaper', 441 'marketing': 'fas fa-bullhorn', 442 'creation': 'fas fa-magic', 443 'productivity': 'fas fa-tasks', 444 'voice': 'fas fa-volume-up', 445 'ocr': 'fas fa-scanner', 446 'data': 'fas fa-database', 447 'writing': 'fas fa-edit' 448 }; 449 favoritesList.innerHTML = favoriteTools.map(tool => { 450 const primaryCategory = tool.categories.split(',')[0]; 451 const iconClass = categoryIcons[primaryCategory] || 'fas fa-robot'; 452 // === [FIX 2] Force English Links for Favorites === 453 // Ø§ÙØªØ¹Ø¯ÙÙ: ÙØªØ رابط Ø§ÙØ£Ø¯Ø§Ø© ٠٠اÙÙ ÙØ¶ÙØ© 454const articleUrl = tool.details && tool.details.website ? tool.details.website : '#'; 455 456 return `<div class="favorite-item"><div class="favorite-item-icon"><i class="${iconClass}"></i></div><div class="favorite-item-details"><h4 class="favorite-item-title">${InputSecurity.sanitizeHTML(tool.name)}</h4><p class="favorite-item-description">${InputSecurity.sanitizeHTML(tool.description)}</p></div><div class="favorite-item-actions"><a href="${articleUrl}" target="_blank" class="favorite-action-button favorite-action-view" title="${UIStrings.viewDetails}"><i class="fas fa-eye"></i></a><button class="favorite-action-button favorite-action-delete" data-tool-id="${tool.id}" title="${UIStrings.remove}">
456<i class="fas fa-trash-alt"></i></button></div></div>`; 457 }).join(''); 458 } 459 460 function initializeEventListeners() { 461 const favoritesModal = document.getElementById('favorites-modal'); 462 const newsletterModal = document.getElementById('newsletter-modal-overlay'); 463 464 document.querySelectorAll('.modal, #favorites-modal, #newsletter-modal-overlay').forEach(modal => { 465 if (modal) { 466 modal.addEventListener('click', function (e) { 467 if (e.target === this || e.target.closest('.close-modal, #close-favorites-modal, #modal-close-btn')) { 468 closeModal(this); 469 } 470 }); 471 } 472 }); 473 474 const favBtn = document.getElementById('favorites-btn'); 475 if (favBtn) { 476 favBtn.addEventListener('click', () => { 477 displayFavorites(); 478 openModal(favoritesModal); 479 }); 480 } 481 482 const favMobileBtn = document.getElementById('favorites-mobile'); 483 if (favMobileBtn) { 484 favMobileBtn.addEventListener('click', (e) => { 485 e.preventDefault(); 486 displayFavorites(); 487 openModal(favoritesModal); 488 const mobileMenu = document.getElementById('mobile-menu'); 489 const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); 490 if (mobileMenu && mobileMenuToggle) { 491 mobileMenu.classList.add('hidden'); 492 mobileMenuToggle.setAttribute('aria-expanded', 'false'); 493 } 494 }); 495 } 496 497 if (favoritesModal) { 498 favoritesModal.addEventListener('click', (e) => { 499 const removeBtn = e.target.closest('.favorite-action-delete'); 500 if (removeBtn) { 501 const toolId = removeBtn.getAttribute('data-tool-id'); 502 toggleFavorite(toolId); 503 displayFavorites(); 504 updateFavoritesCount(); 505 const cardBadge = document.querySelector(`.tool-card .favorites-badge[data-tool-id="${toolId}"]`); 506 if (cardBadge) { 507 cardBadge.classList.remove('active'); 508 cardBadge.querySelector('i').className = 'fa-regular fa-heart'; 509 } 510 } 511 }); 512 } 513 514 document.addEventListener('keydown', (e) => { 515 if (e.key === 'Escape') { 516 document.querySelectorAll('.modal.active, #favorites-modal.active, #newsletter-modal-overlay.active').forEach(closeModal); 517 } 518 }); 519 520 const searchInput = document.getElementById('search-input'); 521 if (searchInput) { 522 searchInput.addEventListener('input', function () { 523 if (!RateLimit.isAllowed('search')) { 524 showNotification(UIStrings.rateLimitExceeded); 525 return; 526 } 527 showSearchSuggestions(this.value.trim()); 528 }); 529 searchInput.addEventListener('keydown', handleSuggestionKeyboard); 530 searchInput.addEventListener('focus', function () { 531 if (this.value.trim().length > 0) { 532 showSearchSuggestions(this.value.trim()); 533 } 534 }); 535 document.addEventListener('click', function (e) { 536 if (!searchInput.closest('.search-container').contains(e.target)) { 537 hideSuggestions(); 538 } 539 }, { 540 passive: true 541 }); 542 } 543 544 setupAdvancedSearchToggle(); 545 setupFilterHandlers(); 546 setupTabHandlers(); 547 setupFormHandlers(); 548 setupNavigationHandlers(); 549 setupUtilityHandlers(); 550 setupThemeHandler(); 551 initializeNewsletterModal(); // Calling the updated function 552 const throttledScrollHandler = PerformanceUtils.throttle(() => { 553 updateBackToTopButton(); 554 checkFadeElements(); 555 }, 100); 556 window.addEventListener('scroll', throttledScrollHandler, { 557 passive: true 558 }); 559 updateFavoritesCount(); 560 displayTotalToolCount(); 561 } 562 563 function toggleFavorite(toolId) { 564 let favorites = getFavorites(); 565 const index = favorites.indexOf(toolId); 566 if (index === -1) { 567 favorites.push(toolId); 568 } else { 569 favorites.splice(index, 1); 570 } 571 SecureStorage.setItem('favorites', favorites); 572 } 573 574 function getFavorites() { 575 return SecureStorage.getItem('favorites') || []; 576 } 577 578 function updateFavoritesCount() { 579 const favoritesCount = getFavorites().length; 580 const countElement = document.getElementById('favorites-count'); 581 if (countElement) { 582 if (favoritesCount > 0) { 583 countElement.textContent = favoritesCount; 584 countElement.classList.remove('hidden'); 585 } else { 586 countElement.classList.add('hidden'); 587 } 588 } 589 } 590
591 let currentSearchTimeout = null; 592 let searchCache = new Map(); 593 594 function performSmartSearch(searchTerm) { 595 const cacheKey = searchTerm; 596 if (searchCache.has(cacheKey)) { 597 return searchCache.get(cacheKey); 598 } 599 if (!searchTerm || searchTerm.length === 0) { 600 return []; 601 } 602 searchTerm = InputSecurity.sanitizeSearchInput(searchTerm.toLowerCase().trim()); 603 const allTools = getAllTools(); 604 const results = []; 605 allTools.forEach(tool => { 606 let score = 0; 607 const toolName = tool.name.toLowerCase(); 608 const toolDesc = tool.description.toLowerCase(); 609 const toolCategories = tool.categories.toLowerCase(); 610 const toolKeywords = tool.keywords ? tool.keywords.map(k => k.toLowerCase()) : []; 611 if (toolName.includes(searchTerm)) { 612 score += toolName.startsWith(searchTerm) ? 500 : 300; 613 } 614 if (toolDesc.includes(searchTerm)) { 615 score += toolDesc.startsWith(searchTerm) ? 200 : 100; 616 } 617 toolKeywords.forEach(keyword => { 618 if (keyword.includes(searchTerm)) { 619 score += 400; 620 } 621 }); 622 if (toolCategories.includes(searchTerm)) { 623 score += 150; 624 } 625 if (score > 0) { 626 results.push({ 627 tool, 628 score 629 }); 630 } 631 }); 632 const sortedResults = results.sort((a, b) => b.score - a.score).slice(0, 12).map(result => result.tool); 633 if (searchCache.size > 50) { 634 searchCache.clear(); 635 } 636 searchCache.set(cacheKey, sortedResults); 637 return sortedResults; 638 } 639 640 function showSearchSuggestions(searchTerm) { 641 const suggestionsContainer = document.getElementById('search-suggestions'); 642 const searchIcon = document.getElementById('search-icon'); 643 const searchLoading = document.getElementById('search-loading'); 644 if (!searchTerm || searchTerm.trim().length === 0) { 645 suggestionsContainer.classList.add('hidden'); 646 suggestionsContainer.classList.remove('visible'); 647 searchLoading.classList.add('hidden'); 648 searchIcon.classList.remove('hidden'); 649 return; 650 } 651 searchIcon.classList.add('hidden'); 652 searchLoading.classList.remove('hidden'); 653 if (currentSearchTimeout) { 654 clearTimeout(currentSearchTimeout); 655 } 656 currentSearchTimeout = setTimeout(() => { 657 const matchingTools = performSmartSearch(searchTerm.trim()); 658 PerformanceUtils.batchDOMUpdates(() => { 659 searchLoading.classList.add('hidden'); 660 searchIcon.classList.remove('hidden'); 661 suggestionsContainer.innerHTML = ''; 662 if (matchingTools.length === 0) { 663 const emptyDiv = document.createElement('div'); 664 emptyDiv.className = 'suggestion-empty'; 665 emptyDiv.innerHTML = `<i class="fas fa-search"></i><div>${UIStrings.noResults}</div><div style="font-size: 12px; margin-top: 4px;">${UIStrings.noResultsHint}</div>`; 666 suggestionsContainer.appendChild(emptyDiv); 667 } else { 668 const fragment = PerformanceUtils.createFragment(); 669 matchingTools.forEach((tool, index) => { 670 fragment.appendChild(createAdvancedSuggestionItem(tool, searchTerm, index)); 671 }); 672 suggestionsContainer.appendChild(fragment); 673 } 674 suggestionsContainer.classList.remove('hidden'); 675 requestAnimationFrame(() => { 676 suggestionsContainer.classList.add('visible'); 677 }); 678 }); 679 }, 200); 680 } 681 682 function createAdvancedSuggestionItem(tool, searchTerm, index) { 683 const div = document.createElement('div'); 684 div.className = 'suggestion-item'; 685 div.dataset.toolId = tool.id; 686 div.dataset.index = index; 687 div.setAttribute('role', 'option'); 688 div.setAttribute('tabindex', '-1'); 689 const primaryCategory = tool.categories.split(',')[0]; 690 const categoryDisplayName = getCategoryDisplayName(primaryCategory); 691 const categoryIcons = { 692 'image': 'fas fa-image', 693 'text': 'fas fa-pen', 694 'code': 'fas fa-code', 695 'video': 'fas fa-video', 696 'audio': 'fas fa-microphone', 697 'docs': 'fas fa-file-alt', 698 'ai': 'fas fa-brain', 699 'art': 'fas fa-palette', 700 'design': 'fas fa-drafting-compass', 701 'development': 'fas fa-laptop-code', 702 'content': 'fas fa-newspaper', 703 'marketing': 'fas fa-bullhorn', 704 'creation': 'fas fa-magic', 705 'productivity': 'fas fa-tasks', 706 'voice': 'fas fa-volume-up', 707 'ocr': 'fas fa-scanner', 708 'data': 'fas fa-database', 709 'writing': 'fas fa-pen' 710 }; 711 const iconClass = categoryIcons[primaryCategory] || 'fas fa-cog';
712 const highlightedName = highlightSearchMatch(tool.name, searchTerm); 713 const highlightedDesc = highlightSearchMatch(tool.description, searchTerm); 714 let subscriptionBadge = ''; 715 const subscriptions = tool.subscription.split(','); 716 if (subscriptions.includes('free') && !subscriptions.includes('paid')) { 717 subscriptionBadge = `<span style="background: #22c55e; color: white; padding: 2px 6px; border-radius: 10px; font-size: 10px; margin-right: 6px;">${UIStrings.free}</span>`; 718 } else if (subscriptions.includes('paid') && !subscriptions.includes('free')) { 719 subscriptionBadge = `<span style="background: #ef4444; color: white; padding: 2px 6px; border-radius: 10px; font-size: 10px; margin-right: 6px;">${UIStrings.paid}</span>`; 720 } else if (subscriptions.includes('freemium')) { 721 subscriptionBadge = `<span style="background: #f59e0b; color: white; padding: 2px 6px; border-radius: 10px; font-size: 10px; margin-right: 6px;">${UIStrings.freemium}</span>`; 722 } 723 div.innerHTML = `<div class="suggestion-icon"><i class="${iconClass}"></i></div><div class="suggestion-content"><div class="suggestion-title">${highlightedName}</div><div class="suggestion-description">${highlightedDesc.substring(0, 80)}${highlightedDesc.length > 80 ? '...' : ''}</div><div style="display: flex; align-items: center; margin-top: 4px;">${subscriptionBadge}<span class="suggestion-category">${InputSecurity.sanitizeHTML(categoryDisplayName)}</span></div></div>`; 724 div.addEventListener('mouseenter', function () { 725 document.querySelectorAll('.suggestion-item.selected').forEach(item => { 726 item.classList.remove('selected'); 727 }); 728 this.classList.add('selected'); 729 }); 730 div.addEventListener('click', function (e) { 731 e.preventDefault(); 732 e.stopPropagation(); 733 hideSuggestions(); 734 735 // === Ø§ÙØªØ¹Ø¯ÙÙ Ø§ÙØ¬Ø¯Ùد: Ø¬ÙØ¨ رابط Ø§ÙØ£Ø¯Ø§Ø© ÙÙØªØÙ ٠باشرة === 736 const toolWebsite = tool.details && tool.details.website ? tool.details.website : '#'; 737 window.open(toolWebsite, '_blank', 'noopener,noreferrer'); 738 739 if (typeof gtag !== 'undefined') { 740 gtag('event', 'search_suggestion_click', { 741 'search_term': searchTerm, 742 'tool_name': tool.name, 743 'position': index + 1 744 }); 745 } 746 }); 747 return div; 748 } 749 750 function highlightSearchMatch(text, searchTerm) { 751 if (!searchTerm || searchTerm.length === 0) return InputSecurity.sanitizeHTML(text); 752 const sanitizedText = InputSecurity.sanitizeHTML(text); 753 const regex = new RegExp(`(${searchTerm.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')})`, 'gi'); 754 return sanitizedText.replace(regex, '<span class="suggestion-highlight">$1</span>'); 755 } 756 757 function hideSuggestions() { 758 const suggestionsContainer = document.getElementById('search-suggestions'); 759 suggestionsContainer.classList.remove('visible'); 760 setTimeout(() => { 761 suggestionsContainer.classList.add('hidden'); 762 }, 300); 763 } 764 765 function handleSuggestionKeyboard(e) { 766 const suggestionsContainer = document.getElementById('search-suggestions'); 767 const suggestionItems = suggestionsContainer.querySelectorAll('.suggestion-item'); 768 if (suggestionItems.length === 0) return; 769 const currentSelected = suggestionsContainer.querySelector('.suggestion-item.selected'); 770 let newIndex = -1; 771 switch (e.key) { 772 case 'ArrowDown': 773 e.preventDefault(); 774 if (currentSelected) { 775 const currentIndex = parseInt(currentSelected.getAttribute('data-index')); 776 newIndex = Math.min(currentIndex + 1, suggestionItems.length - 1); 777 } else { 778 newIndex = 0; 779 } 780 break; 781 case 'ArrowUp': 782 e.preventDefault(); 783 if (currentSelected) { 784 const currentIndex = parseInt(currentSelected.getAttribute('data-index')); 785 newIndex = Math.max(currentIndex - 1, 0); 786 } else { 787 newIndex = suggestionItems.length - 1; 788 } 789 break; 790 case 'Enter': 791 e.preventDefault(); 792 if (currentSelected) { 793 currentSelected.click(); 794 } else { 795 performSearch(); 796 } 797 return; 798 case 'Escape': 799 hideSuggestions(); 800 return; 801 case 'Tab': 802 if (currentSelected) { 803 e.preventDefault(); 804 const searchInput = document.getElementById('search-input'); 805 const toolId = currentSelected.getAttribute('data-tool-id'); 806 const tool = getAllTools().find(t => t.id === toolId); 807 if (tool) { 808 searchInput.value = tool.name; 809 hideSuggestions(); 810 } 811 } 812 return; 813 } 814 if (newIndex >= 0) {
815 suggestionItems.forEach(item => item.classList.remove('selected')); 816 suggestionItems[newIndex].classList.add('selected'); 817 suggestionItems[newIndex].scrollIntoView({ 818 block: 'nearest', 819 behavior: 'smooth' 820 }); 821 } 822 } 823 824 function initializeTools() { 825 filterByTab('all'); 826 } 827 828 function setupUtilityHandlers() { 829 document.getElementById('load-more-tools').addEventListener('click', function () { 830 loadMoreTools(this); 831 }); 832 document.addEventListener('click', function (e) { 833 if (e.target.closest('.favorites-badge')) { 834 handleFavoriteToggle(e.target.closest('.favorites-badge')); 835 } 836 }); 837 const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); 838 const mobileMenu = document.getElementById('mobile-menu'); 839 mobileMenuToggle.addEventListener('click', function () { 840 const isHidden = mobileMenu.classList.contains('hidden'); 841 mobileMenu.classList.toggle('hidden'); 842 this.setAttribute('aria-expanded', isHidden ? 'true' : 'false'); 843 }); 844 } 845 846 // --- NEWSLETTER LOGIC (FIXED) --- 847 function initializeNewsletterModal() { 848 const overlay = document.getElementById('newsletter-modal-overlay'); 849 const form = document.getElementById('newsletter-modal-form'); 850 851 const toggleModal = (show) => { 852 if (!overlay) return; 853 if (show) { 854 document.body.classList.add('modal-open'); 855 overlay.style.display = 'flex'; 856 requestAnimationFrame(() => { 857 overlay.classList.add('active'); 858 overlay.style.opacity = '1'; 859 }); 860 } else { 861 overlay.style.opacity = '0'; 862 overlay.classList.remove('active'); 863 setTimeout(() => { 864 overlay.style.display = 'none'; 865 document.body.classList.remove('modal-open'); 866 }, 300); 867 868 const currentState = SecureStorage.getItem('newsletterState'); 869 if (currentState !== 'subscribed') { 870 SecureStorage.setItem('newsletterState', 'closed'); 871 SecureStorage.setItem('newsletterClosedTimestamp', Date.now()); 872 } 873 } 874 }; 875 876 // Event Delegation for Buttons 877 document.addEventListener('click', (e) => { 878 if (e.target.closest('#newsletter-btn, #newsletter-btn-mobile, #open-newsletter-modal-btn')) { 879 e.preventDefault(); 880 toggleModal(true); 881 } 882 if (e.target.closest('#modal-close-btn') || e.target === overlay) { 883 e.preventDefault(); 884 toggleModal(false); 885 } 886 }); 887 888 if (form) { 889 form.addEventListener('submit', function (e) { 890 e.preventDefault(); 891 const submitBtn = document.getElementById('modal-submit-btn'); 892 const originalBtnHTML = submitBtn.innerHTML; 893 submitBtn.innerHTML = `<div class="loading-spinner mx-auto"></div>`; 894 submitBtn.disabled = true; 895 896 fetch(form.action, { 897 method: 'POST', 898 body: new FormData(form), 899 headers: { 900 'Accept': 'application/json' 901 } 902 }).then(response => { 903 if (response.ok) { 904 SecureStorage.setItem('newsletterState', 'subscribed'); 905 submitBtn.innerHTML = `<i class="fas fa-check"></i>`; 906 showNotification(UIStrings.newsletterSuccess); 907 form.reset(); 908 setTimeout(() => { 909 toggleModal(false); 910 submitBtn.innerHTML = originalBtnHTML; 911 submitBtn.disabled = false; 912 }, 2000); 913 } else { 914 throw new Error('Failed'); 915 } 916 }).catch(() => { 917 showNotification(UIStrings.newsletterError, 'error'); 918 submitBtn.innerHTML = originalBtnHTML; 919 submitBtn.disabled = false;
920 }); 921 }); 922 } 923 setupAutomaticModalTrigger(() => toggleModal(true)); 924 } 925 926 function setupAutomaticModalTrigger(openModalCallback) { 927 const MODAL_COOLING_PERIOD = 7 * 24 * 60 * 60 * 1000; 928 let modalHasBeenTriggered = false; 929 const newsletterState = SecureStorage.getItem('newsletterState'); 930 if (newsletterState === 'subscribed') return; 931 if (newsletterState === 'closed') { 932 const closedTimestamp = SecureStorage.getItem('newsletterClosedTimestamp'); 933 if (closedTimestamp && (Date.now() - closedTimestamp < MODAL_COOLING_PERIOD)) return; 934 } 935 const triggerModal = () => { 936 if (modalHasBeenTriggered) return; 937 openModalCallback(); 938 modalHasBeenTriggered = true; 939 document.body.removeEventListener('mouseout', handleExitIntent); 940 }; 941 const handleExitIntent = (e) => { 942 if (e.clientY <= 0) triggerModal(); 943 }; 944 document.body.addEventListener('mouseout', handleExitIntent); 945 setTimeout(() => { 946 triggerModal(); 947 }, 25000); 948 } 949 950 function setupAdvancedSearchToggle() { 951 const toggle = document.getElementById('advanced-search-toggle'); 952 const options = document.getElementById('advanced-search-options'); 953 const chevronDown = document.getElementById('chevron-down'); 954 const chevronUp = document.getElementById('chevron-up'); 955 toggle.addEventListener('click', function () { 956 const isOpen = options.classList.toggle('open'); 957 chevronDown.classList.toggle('hidden', isOpen); 958 chevronUp.classList.toggle('hidden', !isOpen); 959 this.setAttribute('aria-expanded', isOpen); 960 }); 961 document.getElementById('reset-filters').addEventListener('click', resetFilters); 962 document.getElementById('apply-filters').addEventListener('click', applyFilters); 963 } 964 965 function setupFilterHandlers() { 966 document.addEventListener('click', function (e) { 967 if (e.target.matches('.filter-btn')) { 968 handleFilterButtonClick(e.target, document.querySelectorAll('.filter-btn')); 969 } 970 if (e.target.matches('.quick-filter-btn')) { 971 handleFilterButtonClick(e.target, document.querySelectorAll('.quick-filter-btn')); 972 } 973 }); 974 document.getElementById('search-input').addEventListener('keypress', e => { 975 if (e.key === 'Enter' && !document.querySelector('.suggestion-item.selected')) { 976 performSearch(); 977 } 978 }); 979 document.getElementById('quick-sort').addEventListener('change', e => sortTools(e.target.value)); 980 } 981 982 function handleFilterButtonClick(clickedBtn, allButtons) { 983 allButtons.forEach(btn => { 984 btn.classList.remove('active'); 985 btn.setAttribute('aria-pressed', 'false'); 986 }); 987 clickedBtn.classList.add('active'); 988 clickedBtn.setAttribute('aria-pressed', 'true'); 989 const category = clickedBtn.dataset.category; 990 if (clickedBtn.matches('.filter-btn')) { 991 document.getElementById('tabs-container').scrollIntoView({ 992 behavior: 'smooth' 993 }); 994 const matchingQuickFilter = document.querySelector(`.quick-filter-btn[data-category="${category}"]`); 995 if (matchingQuickFilter) { 996 document.querySelectorAll('.quick-filter-btn').forEach(b => { 997 b.classList.remove('active'); 998 b.setAttribute('aria-pressed', 'false'); 999 }); 1000 matchingQuickFilter.classList.add('active'); 1001 matchingQuickFilter.setAttribute('aria-pressed', 'true'); 1002 } 1003 } 1004 filterByCategory(category); 1005 } 1006 1007 function setupTabHandlers() { 1008 document.addEventListener('click', e => { 1009 if (e.target.matches('.tab-button')) { 1010 document.querySelectorAll('.tab-button').forEach(btn => { 1011 btn.classList.remove('active'); 1012 btn.setAttribute('aria-selected', 'false'); 1013 }); 1014 e.target.classList.add('active'); 1015 e.target.setAttribute('aria-selected', 'true'); 1016 filterByTab(e.target.dataset.tab); 1017 } 1018 }); 1019 } 1020 1021 function setupFormHandlers() { 1022 document.getElementById('contact-form').addEventListener('submit', handleContactFormSubmit); 1023 } 1024 1025 function handleContactFormSubmit(e) { 1026 e.preventDefault(); 1027 const form = e.target; 1028 if (!RateLimit.isAllowed('contact')) { 1029 showNotification(UIStrings.contactRateLimit, 'error'); 1030 return; 1031 } 1032 const data = { 1033 name: form.name.value, 1034 email: form.email.value, 1035 subject: form.subject.value, 1036 message: form.message.value 1037 }; 1038 if (!validateContactForm(data).isValid) { 1039 showNotification(UIStrings.contactInvalid, 'error'); 1040 return; 1041 } 1042 const btn = form.querySelector('button[type="submit"]'); 1043 const originalText = btn.textContent; 1044 btn.innerHTML = '<div class="loading-spinner mx-auto"></div>'; 1045 btn.disabled = true; 1046 fetch(form.action, { 1047 method: form.method, 1048 body: new FormData(form), 1049 headers: { 1050 'Accept': 'application/json' 1051 } 1052 }).then(res => { 1053 btn.disabled = false;
1054 btn.textContent = originalText; 1055 if (res.ok) { 1056 form.reset(); 1057 showNotification(UIStrings.contactSuccess); 1058 } else { 1059 showNotification(UIStrings.contactError, 'error'); 1060 } 1061 }).catch(() => { 1062 btn.disabled = false; 1063 btn.textContent = originalText; 1064 showNotification(UIStrings.networkError, 'error'); 1065 }); 1066 } 1067 1068 function validateContactForm(data) { 1069 const valid = Object.values(data).every(val => InputSecurity.validateFormInput(val, 'name')); 1070 return { 1071 isValid: valid 1072 }; 1073 } 1074 1075 function setupNavigationHandlers() { 1076 document.addEventListener('click', e => { 1077 const navLink = e.target.closest('.nav-link[href^="#"]'); 1078 if (navLink) { 1079 e.preventDefault(); 1080 handleNavigation(navLink.getAttribute('href').substring(1)); 1081 } 1082 const footerLink = e.target.closest('.footer-link[data-target]'); 1083 if (footerLink) { 1084 e.preventDefault(); 1085 handleNavigation(footerLink.dataset.target); 1086 }
1087 const categoryFooterLink = e.target.closest('.category-footer-link[data-category]'); 1088 if (categoryFooterLink) { 1089 e.preventDefault(); 1090 navigateToCategory(categoryFooterLink.dataset.category); 1091 } 1092 }); 1093 } 1094 1095 function setupThemeHandler() { 1096 document.getElementById('theme-toggle').addEventListener('click', toggleTheme); 1097 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { 1098 if (!SecureStorage.getItem('darkMode')) { 1099 document.documentElement.classList.toggle('dark', e.matches); 1100 } 1101 }); 1102 } 1103 1104 function resetFilters() { 1105 document.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false); 1106 document.getElementById('sort-select').value = "popular"; 1107 showNotification(UIStrings.filtersReset); 1108 } 1109 1110 function applyFilters() { 1111 const subFilters = Array.from(document.querySelectorAll('.subscription-filter:checked')).map(cb => cb.value); 1112 const catFilters = Array.from(document.querySelectorAll('.category-filter:checked')).map(cb => cb.value); 1113 const featFilters = Array.from(document.querySelectorAll('.feature-filter:checked')).map(cb => cb.value); 1114 filterAndRender(subFilters, catFilters, featFilters); 1115 showNotification(UIStrings.filtersApplied); 1116 document.getElementById('advanced-search-options').classList.remove('open'); 1117 document.getElementById('chevron-down').classList.remove('hidden'); 1118 document.getElementById('chevron-up').classList.add('hidden'); 1119 document.getElementById('advanced-search-toggle').setAttribute('aria-expanded', 'false'); 1120 } 1121 1122 function filterAndRender(subscriptions = [], categories = [], features = []) { 1123 let filteredTools = getAllTools(); 1124 if (subscriptions.length > 0) { 1125 filteredTools = filteredTools.filter(tool => subscriptions.some(s => tool.subscription.includes(s))); 1126 } 1127 if (categories.length > 0) { 1128 filteredTools = filteredTools.filter(tool => categories.some(c => tool.categories.includes(c))); 1129 } 1130 if (features.length > 0) { 1131 filteredTools = filteredTools.filter(tool => features.every(f => (tool.features || "").includes(f))); 1132 } 1133 renderTools(filteredTools); 1134 } 1135 1136 function toggleTheme() { 1137 const isDark = document.documentElement.classList.toggle('dark'); 1138 SecureStorage.setItem('darkMode', isDark ? 'dark' : 'light'); 1139 showNotification(isDark ? UIStrings.darkMode : UIStrings.lightMode); 1140 } 1141 1142 function handleFavoriteToggle(badge) { 1143 const toolId = badge.dataset.toolId; 1144 const isFavorite = getFavorites().includes(toolId); 1145 toggleFavorite(toolId); 1146 updateFavoritesCount(); 1147 PerformanceUtils.batchDOMUpdates(() => { 1148 badge.classList.toggle('active', !isFavorite); 1149 badge.querySelector('i').className = `fa-${!isFavorite ? 'solid' : 'regular'} fa-heart`; 1150 badge.setAttribute('aria-label', !isFavorite ? UIStrings.removeFavorite : UIStrings.addFavorite); 1151 }); 1152 if (document.querySelector('.tab-button.active')?.dataset.tab === 'favorites') { 1153 filterByTab('favorites'); 1154 } 1155 showNotification(!isFavorite ? UIStrings.addedToFav : UIStrings.removedFromFav); 1156 } 1157 1158 function updateBackToTopButton() { 1159 document.getElementById('backToTopBtn').classList.toggle('visible', window.pageYOffset > 300); 1160 } 1161 1162 function navigateToCategory(category) { 1163 document.getElementById('tabs-container').scrollIntoView({ 1164 behavior: 'smooth' 1165 }); 1166 const quickFilter = document.querySelector(`.quick-filter-btn[data-category="${category}"]`); 1167 if (quickFilter) { 1168 document.querySelectorAll('.quick-filter-btn').forEach(b => { 1169 b.classList.remove('active'); 1170 b.setAttribute('aria-pressed', 'false'); 1171 }); 1172 quickFilter.classList.add('active'); 1173 quickFilter.setAttribute('aria-pressed', 'true'); 1174 } 1175 filterByCategory(category); 1176 } 1177 1178 function updateResultsCount(count, total) { 1179 const resultsText = document.getElementById('results-count');
1180 if (resultsText) resultsText.innerHTML = `Showing <span class="font-medium text-white">${count}</span> of ${total} tools`; 1181 } 1182 1183 function performSearch() { 1184 const searchTerm = document.getElementById('search-input').value.trim().toLowerCase(); 1185 if (searchTerm === '') { 1186 showNotification(UIStrings.enterSearchTerm); 1187 return; 1188 } 1189 if (!RateLimit.isAllowed('search')) { 1190 showNotification(UIStrings.rateLimitExceeded); 1191 return; 1192 } 1193 const matchingTools = performSmartSearch(searchTerm); 1194 renderTools(matchingTools); 1195 showNotification(UIStrings.searchResult.replace('{count}', matchingTools.length).replace('{term}', searchTerm)); 1196 document.getElementById('tabs-container').scrollIntoView({ 1197 behavior: 'smooth' 1198 }); 1199 hideSuggestions(); 1200 } 1201 1202 function filterByCategory(category) { 1203 let toolsToShow = getToolsByCategory(category); 1204 renderTools(toolsToShow); 1205 } 1206 1207 function filterByTab(tab) { 1208 document.querySelectorAll('.quick-filter-btn').forEach(b => { 1209 b.classList.remove('active'); 1210 b.setAttribute('aria-pressed', 'false'); 1211 }); 1212 const allBtn = document.querySelector('.quick-filter-btn[data-category="all"]'); 1213 if (allBtn) { 1214 allBtn.classList.add('active'); 1215 allBtn.setAttribute('aria-pressed', 'true'); 1216 } 1217 let toolsToShow = getToolsByTab(tab); 1218 renderTools(toolsToShow); 1219 } 1220 1221 function renderTools(tools) { 1222 const toolsContainer = document.getElementById('tools-container'); 1223 const displayTools = tools.slice(0, 9); 1224 1225 toolsContainer.innerHTML = ''; // Ù Ø³Ø Ø§ÙÙØ¯ÙÙ 1226 1227 if (displayTools.length === 0) { 1228 toolsContainer.innerHTML = `<div class="col-span-1 md:col-span-2 lg:col-span-3 text-center py-12 text-gray-400"><i class="fas fa-search text-5xl mb-4"></i><p class="font-semibold text-lg">${UIStrings.noFilterResults}</p></div>`; 1229 } else { 1230 const fragment = PerformanceUtils.createFragment(); 1231 displayTools.forEach((tool, i) => fragment.appendChild(createToolCard(tool, (i % 3) * 100))); 1232 toolsContainer.appendChild(fragment); 1233 } 1234 1235 updateResultsCount(displayTools.length, tools.length); 1236
1237 const loadMoreBtn = document.getElementById('load-more-tools'); 1238 if (tools.length <= 9) { 1239 loadMoreBtn.style.display = 'none'; 1240 } else { 1241 loadMoreBtn.style.display = 'flex'; 1242 loadMoreBtn.disabled = false; 1243 const remaining = tools.length - displayTools.length; 1244 const toLoad = Math.min(9, remaining); 1245 loadMoreBtn.innerHTML = `<span>${UIStrings.loadMore.replace('{count}', toLoad)}</span>${loadMoreArrowSVG}`; 1246 } 1247 1248 // === ð ÙØ°Ø§ ÙÙ Ø§ÙØ³Ø·Ø± Ø§ÙØ³ØØ±Ù ÙÙØ¥ØµÙØ§Ø ð === 1249 // ÙØ¬Ø¨Ø± اÙÙ ØªØµÙØ عÙÙ ÙØØµ Ø§ÙØ¹Ùاصر ÙÙØ±Ø§Ù بعد Ø¥Ø¶Ø§ÙØªÙا 1250 setTimeout(() => { 1251 checkFadeElements(); 1252 }, 50); 1253 } 1254 1255 function loadMoreTools(button) { 1256 button.innerHTML = '<div class="loading-spinner mx-auto"></div>'; 1257 button.disabled = true; 1258 const toolsContainer = document.getElementById('tools-container'); 1259 const currentCount = toolsContainer.children.length; 1260 const activeCategory = document.querySelector('.quick-filter-btn.active').dataset.category; 1261 const activeTab = document.querySelector('.tab-button.active').dataset.tab; 1262 let allFilteredTools; 1263 if (activeCategory === 'all') { 1264 allFilteredTools = getToolsByTab(activeTab); 1265 } else { 1266 const base = getToolsByCategory(activeCategory); 1267 allFilteredTools = (activeTab === 'favorites') ? base.filter(t => getFavorites().includes(t.id)) : base.filter(t => t.tab && t.tab.includes(activeTab)); 1268 } 1269 const nextTools = allFilteredTools.slice(currentCount, currentCount + 9); 1270 setTimeout(() => { 1271 const fragment = PerformanceUtils.createFragment(); 1272 nextTools.forEach((tool, i) => fragment.appendChild(createToolCard(tool, (i % 3) * 100))); 1273 toolsContainer.appendChild(fragment); 1274 checkFadeElements(); 1275 const totalVisible = toolsContainer.children.length; 1276 updateResultsCount(totalVisible, allFilteredTools.length); 1277 if (totalVisible >= allFilteredTools.length) { 1278 button.innerHTML = `<span>${UIStrings.allLoaded}</span>`; 1279 button.disabled = true; 1280 } else { 1281 const remaining = allFilteredTools.length - totalVisible; 1282 const toLoad = Math.min(9, remaining); 1283 button.innerHTML = `<span>${UIStrings.loadMore.replace('{count}', toLoad)}</span>${loadMoreArrowSVG}`; 1284 button.disabled = false; 1285 } 1286 }, 800); 1287 } 1288 1289 function sortTools(sortMethod) { 1290 const toolCards = Array.from(document.querySelectorAll('#tools-container > .tool-card')); 1291 toolCards.sort((a, b) => { 1292 const toolA = getAllTools().find(t => t.id === a.dataset.toolId); 1293 const toolB = getAllTools().find(t => t.id === b.dataset.toolId); 1294 switch (sortMethod) { 1295 case 'newest': 1296 return new Date(toolB.dateAdded || 0) - new Date(toolA.dateAdded || 0); 1297 case 'name-asc': 1298 return toolA.name.localeCompare(toolB.name); 1299 default: 1300 return 0; 1301 } 1302 }); 1303 const toolsContainer = document.getElementById('tools-container'); 1304 toolsContainer.innerHTML = ''; 1305 toolCards.forEach(card => toolsContainer.appendChild(card)); 1306 } 1307 1308 function handleNavigation(target) { 1309 const element = document.getElementById(target === 'home' ? '' : `${target}-section`); 1310 if (element) element.scrollIntoView({ 1311 behavior: 'smooth' 1312 }); 1313 else if (target === 'home') window.scrollTo({ 1314 top: 0, 1315 behavior: 'smooth' 1316 }); 1317 // === FIX 6: Force Blog Link to English === 1318 else if (target === 'blog') window.location.href = 'blog/index.html'; 1319 } 1320 1321 function startCounting() { 1322 document.querySelectorAll('.counter').forEach(counter => { 1323 const target = +counter.dataset.target; 1324 let count = 0; 1325 const inc = Math.ceil(target / 100); 1326 const update = () => { 1327 if (count < target) { 1328 count += inc; 1329 counter.innerText = Math.min(count, target).toLocaleStr
1329ing(currentLang === 'ar' ? 'ar-EG' : 'en-US'); 1330 requestAnimationFrame(update); 1331 } else { 1332 counter.innerText = target.toLocaleString(currentLang === 'ar' ? 'ar-EG' : 'en-US'); 1333 } 1334 }; 1335 update(); 1336 }); 1337 } 1338 1339 function checkFadeElements() { 1340 // Ø§Ø³ØªØ®Ø¯Ù ÙØ§ +100 Ø¨ÙØ³Ù ÙØ¶Ù ا٠ظÙÙØ± Ø§ÙØ¹Ùاصر ØØªÙ ÙÙ ÙØ§Ùت Ù٠أسÙÙ Ø§ÙØ´Ø§Ø´Ø© ÙÙÙÙØ§Ù 1341 const triggerBottom = window.innerHeight + 100; 1342 1343 document.querySelectorAll('.fade-in:not(.appear)').forEach(el => { 1344 const boxTop = el.getBoundingClientRect().top; 1345 1346 // إذا ÙØ§Ù Ø§ÙØ¹Ùصر Ø¸Ø§ÙØ±Ø§Ù ÙÙ Ø§ÙØ´Ø§Ø´Ø© (Ø£Ù ÙØ±Ùبا٠جداÙ)Ø Ø£Ø¸ÙØ±Ù ÙÙØ±Ø§Ù 1347 if (boxTop < triggerBottom) { 1348 // Ø¥Ø¶Ø§ÙØ© ØªØ£Ø®ÙØ± Ø¨Ø³ÙØ· Ø¨ÙØ§Ø¡Ù عÙÙ ØªØ±ØªÙØ¨ Ø§ÙØ¹Ùصر ÙÙØ¹Ø·Ù ØªØ£Ø«ÙØ± تتابع ج٠ÙÙ 1349 setTimeout(() => { 1350 el.classList.add('appear'); 1351 }, el.dataset.delay || 0); 1352 } 1353 }); 1354 } 1355 1356 function displayTotalToolCount() { 1357 const el = document.getElementById('total-tools-display'); 1358 if (!el) return; 1359 const total = getAllTools().length; 1360 el.textContent = UIStrings.totalTools.replace('{count}', total); 1361 } 1362 1363 function generateStructuredData(tools) { 1364 const faqQuestions = { 1365 'en': [{ 1366 "@type": "Question", 1367 "name": "What is the AI Tools Hub?", 1368 "acceptedAnswer": { 1369 "@type": "Answer", 1370 "text": "AI Tools Hub is a curated directory that collects, categorizes, and reviews the best and latest artificial intelligence tools available on the market." 1371 } 1372 }, { 1373 "@type": "Question", 1374 "name": "Are the tools listed on this site free?", 1375 "acceptedAnswer": { 1376 "@type": "Answer", 1377 "text": "We feature a variety of tools, including free, freemium, and paid options. Each tool card is clearly marked with its pricing model so you can easily find tools that fit your budget." 1378 } 1379 }], 1380 'ar': [{ 1381 "@type": "Question", 1382 "name": "٠ا Ù٠دÙÙÙ Ø£Ø¯ÙØ§Øª Ø§ÙØ°Ùاء Ø§ÙØ§ØµØ·ÙØ§Ø¹ÙØ", 1383 "acceptedAnswer": { 1384 "@type": "Answer", 1385 "text": "دÙÙÙ Ø£Ø¯ÙØ§Øª Ø§ÙØ°Ùاء Ø§ÙØ§ØµØ·Ùاع٠Ù٠دÙÙÙ Ù ÙØ³Ù ÙØ¬Ù ع ÙÙØµÙÙ ÙÙØ±Ø§Ø¬Ø¹ Ø£ÙØ¶Ù ÙØ£ØØ¯Ø« Ø£Ø¯ÙØ§Øª Ø§ÙØ°Ùاء Ø§ÙØ§ØµØ·Ùاع٠اÙÙ ØªØ§ØØ© ÙÙ Ø§ÙØ³ÙÙ." 1386 } 1387 }, { 1388 "@type": "Question", 1389 "name": "ÙÙ Ø§ÙØ£Ø¯Ùات اÙ٠درجة ÙÙ ÙØ°Ø§ اÙÙ ÙÙØ¹ ٠جاÙÙØ©Ø", 1390 "acceptedAnswer": { 1391 "@type": "Answer", 1392 "text": "ÙØÙ ÙØ¹Ø±Ø¶ Ù Ø¬Ù ÙØ¹Ø© ٠تÙÙØ¹Ø© Ù Ù Ø§ÙØ£Ø¯ÙØ§ØªØ Ø¨Ù Ø§ Ù٠ذÙÙ Ø§ÙØ®Ùارات اÙ٠جاÙÙØ©Ø ÙØ§ÙÙØ±ÙÙ ÙÙÙ Ø ÙØ§Ù٠دÙÙØ¹Ø©. ÙÙ Ø¨Ø·Ø§ÙØ© أداة تØÙ Ù Ø¹ÙØ§Ù Ø© ÙØ§Ø¶ØØ© ÙÙÙ ÙØ°Ø¬ Ø§ÙØªØ³Ø¹Ùر Ø§ÙØ®Ø§Øµ Ø¨ÙØ§ ØØªÙ تت٠Ù٠بسÙÙÙØ© Ù Ù Ø§ÙØ¹Ø«Ùر عÙÙ Ø§ÙØ£Ø¯Ùات Ø§ÙØªÙ ØªÙØ§Ø³Ø¨ Ù ÙØ²Ø§ÙÙØªÙ." 1393 } 1394 }] 1395 }; 1396 const faqPage = { 1397 "@context": "https://schema.org", 1398 "@type": "FAQPage", 1399 "mainEntity": faqQuestions[currentLang] 1400 }; 1401 document.getElementById('faq-structured-data').textContent = JSON.stringify(faqPage); 1402 const itemList = { 1403 "@context": "https://schema.org", 1404 "@type": "ItemList", 1405 "name": "AI Tools Directory", 1406 "numberOfItems": tools.length, 1407 "itemListElement": tools.slice(0, 10).map((tool, index) => ({ 1408 "@type": "ListItem", 1409 "position": index + 1, 1410 "item": { 1411 "@type": "SoftwareApplication", 1412 "name": tool.name, 1413 "description": tool.description, 1414 "url": tool.details.website, 1415 "applicationCategory": "BusinessApplication", 1416 "operatingSystem": "Web-based", 1417 "brand": { 1418 "@type": "Brand", 1419 "name": tool.details.company 1420 } 1421 } 1422 })) 1423 }; 1424 document.getElementById('itemlist-structured-data').textContent = JSON.stringify(itemList); 1425 } 1426 1427 function initBreakingNewsTicker() { 1428 const tickerEl = document.getElementById('news-ticker'); 1429 const breakingNewsSection = document.getElementById('breaking-news'); 1430 if (!tickerEl || !breakingNewsSection) return; 1431 1432 const news = ToolsData.breakingNews || []; 1433 if (news.length === 0) { 1434 breakingNewsSection.classList.add('hidden'); 1435 return; 1436 } 1437 breakingNewsSection.classList.remove('hidden'); 1438 1439 // === Ø§ÙØªØ¹Ø¯ÙÙ Ø§ÙØ¬Ø¯Ùد: ÙØ±Ø§Ø¡Ø© Ø§ÙØ±Ø§Ø¨Ø· اÙ٠باشر (url) Ø¨Ø¯ÙØ§Ù ٠٠٠سار اÙÙ ÙØ§Ùات === 1440 const newsItemsHTML = news.map(item => { 1441 // ÙØ³ØªØ®Ø¯Ù item.url Ø§ÙØ°Ù ÙØ¶Ø¹Ùا٠ÙÙ Ø§ÙØ¨ÙØ§ÙØ§ØªØ ÙØ¥Ø°Ø§ ÙÙ ÙÙØ¬Ø¯ ÙØ¶Ø¹ '#' 1442 const linkUrl = item.url ? item.url : '#'; 1443 return `<a href="${linkUrl}" class="news-ticker-item">${InputSecurity.sanitizeHTML(item.text)}</a>`; 1444 }).join(''); 1445 1446 // ÙÙØ³Ø® اÙÙ ØØªÙ٠٠رتÙÙ ÙØ¶Ù ا٠است٠رار Ø§ÙØØ±ÙØ© (Infinite loop) 1447 tickerEl.innerHTML = newsItemsHTML + newsItemsHTML; 1448 tickerEl.style.animationDuration = `${news.length * 15}s`; 1449 } 1450 1451 function setupBreakingNewsControls() { 1452 const btn = document.getElementById('ticker-pause-btn'); 1453 const ticker = document.getElementById('news-ticker'); 1454 const section = document.getElementById('breaking-news'); 1455 if (!btn || !ticker || !section) return; 1456 const icon = btn.querySelector('i'); 1457 btn.addEventListener('click', () => { 1458 const isPaused = section.classList.contains('paused'); 1459 section.classList.toggle('paused', !isPaused); 1460 icon.className = `fas fa-${isPaused ? 'pause' : 'play'} text-xs`; 1461 ticker.style.animationPlayState = isPaused ? 'running' : 'paused'; 1462 btn.setAttribute('aria-label', isPaused ? UIStrings.pauseTicker : UIStrings.playTicker); 1463 }); 1464 } 1465 1466 // --- Main Execution --- 1467 getAllTools.cache = null; 1468 initializeTools(); 1469 checkFadeElements(); 1470 initializeEventListeners(); 1471 const statsSection = document.querySelector('.counter'); 1472 if (statsSection) { 1473 new IntersectionObserver((entries, observer) => {
1474 entries.forEach(entry => { 1475 if (entry.isIntersecting) { 1476 startCounting(); 1477 observer.unobserve(entry.target); 1478 } 1479 }); 1480 }, { 1481 threshold: 0.5 1482 }).observe(statsSection); 1483 } 1484 const backToTopBtn = document.getElementById('backToTopBtn'); 1485 if (backToTopBtn) backToTopBtn.addEventListener('click', () => window.scrollTo({ 1486 top: 0, 1487 behavior: 'smooth' 1488 })); 1489 initBreakingNewsTicker(); 1490 setupBreakingNewsControls(); 1491 generateStructuredData(getAllTools()); 1492 1493 1494 1495 1496 // تشغÙ٠اÙÙÙØ¯ 1497 celebrate2026(); 1498});
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.