diff --git a/search_analyzer.js b/search_analyzer.js index a39fc55..ee92b33 100644 --- a/search_analyzer.js +++ b/search_analyzer.js @@ -44,40 +44,40 @@ }; try { - // The most robust way to extract data on LinkedIn is reading the visible text lines. - // Profile cards always follow: Name -> Degree -> Headline -> Location -> ... - const rawLines = cardEl.innerText.split('\n').map(s => s.trim()).filter(s => s.length > 1); + // 1. Name: The most reliable way is the profile link + const profileLinks = Array.from(cardEl.querySelectorAll('a[href*="/in/"]')) + .filter(a => a.innerText.trim().length > 2 && !a.innerText.includes('LinkedIn')); - // Remove lines that are just action buttons or connection degrees - const lines = rawLines.filter(s => { - const low = s.toLowerCase(); - return !low.includes('degree connection') && - !['1st', '2nd', '3rd', '3rd+'].includes(low) && - !low.includes('connect') && - !low.includes('message') && - !low.includes('pending') && - !low.includes('follow') && - !low.includes('view profile'); - }); - - if (lines.length > 0) data.name = lines[0]; - if (lines.length > 1) data.headline = lines[1]; - if (lines.length > 2) data.location = lines[2]; - - // Summary is everything else - if (lines.length > 3) { - data.summary = lines.slice(3, 8).join(' | '); + if (profileLinks.length > 0) { + // Remove any injected language tags + let rawName = profileLinks[0].innerText.trim().split('\n')[0]; + data.name = rawName.replace(/English \(Australia\)|Auto|Translate/gi, '').trim(); + } else { + const nameEl = cardEl.querySelector('.entity-result__title-text, .search-result__title, span[dir="ltr"], h3'); + if (nameEl) data.name = nameEl.innerText.trim().split('\n')[0].replace(/English \(Australia\)|Auto/gi, '').trim(); } - // Cleanup name if it accidentally caught something weird - if (data.name && data.name.includes('LinkedIn')) { + // 2. Headline + const headlineEl = cardEl.querySelector('.entity-result__primary-subtitle, [class*="subtitle"], .linked-area'); + if (headlineEl) data.headline = headlineEl.innerText.trim(); + + // 3. Location + const locationEl = cardEl.querySelector('.entity-result__secondary-subtitle, .search-result__info'); + if (locationEl) data.location = locationEl.innerText.trim(); + + // 4. Summary + const summaryEl = cardEl.querySelector('.entity-result__summary, .search-result__snippets'); + if (summaryEl) data.summary = summaryEl.innerText.trim(); + + // Clean up + if (data.name) data.name = data.name.replace(/View .* profile/gi, '').trim(); + + if (!data.name) { data.name = 'مستثمر محتمل'; } - } catch (e) { console.error('[LJA] Extraction failed', e); data.name = 'مستثمر محتمل'; - data.summary = cardEl.innerText ? cardEl.innerText.substring(0, 300) : ''; } return data; @@ -86,37 +86,40 @@ // ─── Find Cards Logic ────────────────────────────────────────────────────── function findCards() { let cards = []; + let uniqueCards = new Set(); - // LinkedIn search results are usually list items with an image and an action button - let listItems = Array.from(document.querySelectorAll('li, .reusable-search__result-container, .search-entity')); + // Method 1: The official LinkedIn search result container class + let containerElements = document.querySelectorAll('.reusable-search__result-container, .search-entity, .entity-result__item'); + if (containerElements.length > 0) { + containerElements.forEach(el => uniqueCards.add(el)); + console.log('[LJA] Found cards via container classes:', uniqueCards.size); + } - // Filter to only those that look like profile cards - let validCards = listItems.filter(el => { - const txt = el.innerText.toLowerCase(); - // Must have significant text, an image, and a typical LinkedIn action button - const hasAction = txt.includes('connect') || txt.includes('message') || txt.includes('pending') || txt.includes('follow'); - const hasImage = el.querySelector('img'); - const isNotSidebar = !txt.includes('about') && !txt.includes('accessibility'); - return txt.length > 50 && hasAction && hasImage && isNotSidebar; - }); - - if (validCards.length > 0) { - cards = validCards; - console.log('[LJA] Found cards via strict heuristics:', cards.length); - } - - // Deduplicate cards by their text content to avoid nested matches - const uniqueCards = []; - const seenText = new Set(); - for (const card of cards) { - const txt = card.innerText.trim().substring(0, 100); // use first 100 chars as signature - if (!seenText.has(txt)) { - seenText.add(txt); - uniqueCards.push(card); - } + // Method 2: Fallback to finding profile links and going up to the list item + if (uniqueCards.size === 0) { + let profileLinks = Array.from(document.querySelectorAll('a[href*="/in/"]')) + .filter(a => a.innerText.trim().length > 2 && !a.querySelector('img')); + + profileLinks.forEach(link => { + // Find the nearest list item or large div container + let container = link.closest('li') || link.closest('div.mb1') || link.parentElement.parentElement.parentElement; + if (container && container.innerText.length > 20) { + uniqueCards.add(container); + } + }); + console.log('[LJA] Found cards via profile links:', uniqueCards.size); } - return uniqueCards; + // Convert Set to Array and filter out translation extension dropdowns + cards = Array.from(uniqueCards).filter(card => { + const txt = card.innerText.toLowerCase(); + // Must not be a language selector dropdown + if (txt.includes('english (australia)') && txt.length < 50) return false; + // Should have some decent amount of text + return txt.length > 20; + }); + + return cards; } // ─── Inject UI into Card ─────────────────────────────────────────────────