LinkedIn Job Info Copier

Adds a button to copy LinkedIn job post information to the clipboard.

// ==UserScript==
// @name         LinkedIn Job Info Copier
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds a button to copy LinkedIn job post information to the clipboard.
// @author       Lich_Amnesia
// @match        https://www.linkedin.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=linkedin.com
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const BUTTON_ID = 'copy-job-info-button';
    const BUTTON_TEXT = '📋 Copy Job Info';
    const BUTTON_COPIED_TEXT = '✅ Copied!';

    // --- Helper function to safely get text content ---
    function safeGetText(selector, element = document) {
        try {
            const el = element.querySelector(selector);
            // Prioritize innerText for better representation of rendered text, fallback to textContent
            return el ? (el.innerText || el.textContent || '').trim() : 'N/A';
        } catch (e) {
            console.error(`Error getting text for selector "${selector}":`, e);
            return 'Error';
        }
    }

    // --- Helper function to safely get text from multiple elements ---
     function safeGetAllText(selector, separator = '\n- ', element = document) {
        try {
            const els = element.querySelectorAll(selector);
            if (els.length === 0) return 'N/A';
            return Array.from(els).map(el => (el.innerText || el.textContent || '').trim()).filter(Boolean).join(separator);
        } catch (e) {
            console.error(`Error getting all text for selector "${selector}":`, e);
            return 'Error';
        }
    }

    // --- Function to extract job details ---
    function getJobInfo() {
        const topCardSelector = '.job-details-jobs-unified-top-card__container--two-pane'; // More specific container
        const topCardElement = document.querySelector(topCardSelector);

        if (!topCardElement) {
             console.warn("LinkedIn Job Info Copier: Top card element not found yet.");
             return null; // Element not ready
        }


        const jobTitle = safeGetText('.job-details-jobs-unified-top-card__job-title h1 a', topCardElement) || safeGetText('.job-details-jobs-unified-top-card__job-title h1', topCardElement); // Handle both cases h1>a or just h1
        const companyName = safeGetText('.job-details-jobs-unified-top-card__company-name a', topCardElement);
        const tertiaryInfo = safeGetText('.job-details-jobs-unified-top-card__primary-description-container .job-details-jobs-unified-top-card__tertiary-description-container', topCardElement).replace(/\s*·\s*/g, ' | '); // Location, Posted, Applicants

        // Extract pills (Salary, Hybrid, Full-time etc.)
        const pillsRaw = safeGetAllText('.job-details-preferences-and-skills__pill span.ui-label', topCardElement, ' | ');
        // Attempt to split Salary/Benefits from other pills if structure is consistent
        let salary = 'N/A';
        let detailsPills = [];
        const pillElements = topCardElement.querySelectorAll('.job-details-preferences-and-skills__pill span.ui-label');
        pillElements.forEach(pill => {
            const text = (pill.innerText || pill.textContent || '').trim();
            if (text.includes('$') || text.toLowerCase().includes('salary') || text.toLowerCase().includes('/yr')) {
                salary = text;
            } else if (!text.toLowerCase().includes('skills match')) { // Exclude the 'skills match' pill
                detailsPills.push(text);
            }
        });
        const otherDetails = detailsPills.join(' | ') || 'N/A';


        // Job Description
        const jobDescriptionElement = document.querySelector('#job-details .jobs-box__html-content') || document.querySelector('#job-details'); // Try specific content div first
        let jobDescription = 'N/A';
        if (jobDescriptionElement) {
            // Clone to avoid modifying the original, remove the "About the job" heading if present
            const clone = jobDescriptionElement.cloneNode(true);
            const heading = clone.querySelector('h2');
            if (heading && (heading.innerText || heading.textContent || '').trim().toLowerCase() === 'about the job') {
                heading.remove();
            }
             // Try to get innerText for better formatting, fallback to textContent
            jobDescription = (clone.innerText || clone.textContent || '').trim().replace(/\n{3,}/g, '\n\n'); // Reduce excessive newlines
        }


        const jobUrl = window.location.href;

        // Format the output
        const formattedInfo = `Job Title: ${jobTitle}\n` +
                              `Company: ${companyName}\n` +
                              `Location/Info: ${tertiaryInfo}\n` +
                              `Salary/Compensation: ${salary}\n` +
                              `Type/Mode: ${otherDetails}\n` +
                              `URL: ${jobUrl}\n\n` +
                              `---------------- Job Description ----------------\n` +
                              `${jobDescription}`;

        return formattedInfo;
    }

    // --- Function to copy text to clipboard and provide feedback ---
    function copyInfoToClipboard(button) {
        const jobInfoText = getJobInfo();
        if (!jobInfoText) {
             alert("Could not extract job information. The page structure might have changed or not fully loaded.");
             return;
        }

        GM_setClipboard(jobInfoText, 'text');

        // Provide feedback
        const originalText = button.textContent;
        button.textContent = BUTTON_COPIED_TEXT;
        button.disabled = true;
        button.classList.add('copied');

        setTimeout(() => {
            button.textContent = originalText;
            button.disabled = false;
            button.classList.remove('copied');
        }, 2000); // Revert after 2 seconds
    }

    // --- Function to create and add the button ---
    function addCopyButton() {
         // Check if button already exists
        if (document.getElementById(BUTTON_ID)) {
            return;
        }

        // Find the container for the "Easy Apply" / "Save" buttons within the top card
        // Look for the div containing buttons like '.jobs-apply-button' or '.jobs-save-button'
        const buttonContainer = document.querySelector('.job-details-jobs-unified-top-card__container--two-pane .mt4 .display-flex');

        if (buttonContainer) {
            const copyButton = document.createElement('button');
            copyButton.id = BUTTON_ID;
            copyButton.textContent = BUTTON_TEXT;
            // Try to mimic LinkedIn button styles
            copyButton.className = 'artdeco-button artdeco-button--secondary artdeco-button--3'; // Using secondary style like 'Save'
            copyButton.style.marginLeft = '8px'; // Add some space

            copyButton.addEventListener('click', () => {
                copyInfoToClipboard(copyButton);
            });

            // Insert after the Easy Apply button if it exists, otherwise append
            const applyButton = buttonContainer.querySelector('.jobs-s-apply');
             if (applyButton && applyButton.nextSibling) {
                 buttonContainer.insertBefore(copyButton, applyButton.nextSibling);
             } else if (applyButton) {
                 buttonContainer.appendChild(copyButton); // Append if apply is last
             }
             else {
                // Fallback: just append if structure is unexpected
                 buttonContainer.appendChild(copyButton);
            }
            console.log("LinkedIn Job Info Copier: Button added successfully.");

        } else {
            console.warn("LinkedIn Job Info Copier: Button container not found. Cannot add button.");
        }
    }

    // --- Add custom styles for the button ---
    GM_addStyle(`
        #${BUTTON_ID} {
            /* Add any specific overrides if needed */
            cursor: pointer;
        }
        #${BUTTON_ID}.copied {
            background-color: #dff0d8 !important; /* Light green background */
            color: #3c763d !important; /* Darker green text */
            border-color: #d6e9c6 !important;
        }
    `);

    // --- Use MutationObserver to wait for the job details section to load ---
    // LinkedIn loads content dynamically, so we need to wait.
    const observerTargetSelector = '.job-view-layout.jobs-details'; // Main container for job view
    let observer = null;

    function startObserver() {
        const targetNode = document.querySelector(observerTargetSelector);
        if (!targetNode) {
            // If the main container isn't even there yet, wait a bit longer
             console.log("LinkedIn Job Info Copier: Waiting for main job view container...");
             setTimeout(startObserver, 500);
             return;
        }

        console.log("LinkedIn Job Info Copier: Observer started.");
        observer = new MutationObserver((mutationsList, obs) => {
            // Check if the button container is now available and the button isn't already added
            const buttonContainer = document.querySelector('.job-details-jobs-unified-top-card__container--two-pane .mt4 .display-flex');
             if (buttonContainer && !document.getElementById(BUTTON_ID)) {
                 console.log("LinkedIn Job Info Copier: Target container appeared, adding button.");
                 addCopyButton();
                 // Optional: Could disconnect observer here if the button container won't be removed/re-added
                 // obs.disconnect();
             }
             // Also check if the job description is loaded, sometimes it loads later
             const description = document.querySelector('#job-details .jobs-box__html-content');
              if (description && buttonContainer && !document.getElementById(BUTTON_ID)) {
                   console.log("LinkedIn Job Info Copier: Description appeared, ensuring button exists.");
                   addCopyButton(); // Try adding again just in case
             }
        });

        observer.observe(targetNode, {
            childList: true, // Watch for additions/removals of children
            subtree: true    // Watch descendants as well
        });

         // Initial attempt in case content is already present when script runs
         addCopyButton();
    }

    // --- Start the process ---
    // Use window.onload or setTimeout as a fallback if observer setup fails immediately
     if (document.readyState === 'complete') {
         startObserver();
     } else {
         window.addEventListener('load', startObserver);
     }
     // Extra fallback timeout
      setTimeout(addCopyButton, 3000); // Attempt to add after 3 seconds regardless


})();