[
  {
    "path": ".gitignore",
    "content": "npm-debug.log\nnode_modules/\ndist/\ntmp/\n\n.DS_STORE\n.idea\n"
  },
  {
    "path": "README.md",
    "content": "# PayCheck for Twitter\n\ntl;dr - I made an extension that (poorly) estimates how much money a tweet is worth\n\n[Now on the Chrome Web Store](https://chrome.google.com/webstore/detail/paycheck-for-twitter/ldgffedhocinnolmaaecnppdfmmofilp)\n\n# HOW TO INSTALL MANUALLY\n\n1. Download latest .zip from [releases](https://github.com/t3dotgg/paycheck-extension/releases)\n2. Unzip the file\n3. Go to `chrome://extensions/` in your browser\n4. Enable developer mode\n5. Click \"Load unpacked\"\n6. Select the folder you unzipped\n7. Copilot wrote these instructions for me so I hope they're good enough\n"
  },
  {
    "path": "main.js",
    "content": "function convertToRawCount(internationalInputString) {\n  const numberPattern = /([\\d,.]+)([kmb]*)/i;\n  const matches = internationalInputString.match(numberPattern);\n\n  if (!matches) {\n    return NaN; // Return NaN if the input doesn't match the expected pattern\n  }\n\n  const numericPart = matches[1];\n  const multiplier = matches[2].toLowerCase();\n\n  let numericValue;\n\n  const lastChars = [\n    numericPart.slice(-1),\n    numericPart.slice(-2, -1),\n    numericPart.slice(-3, -2),\n  ];\n\n  // Check if second or third to last character are , or . to handle international numbers\n  if (lastChars.includes(\".\") || lastChars.includes(\",\")) {\n    const parts = numericPart.replace(\",\", \".\").split(\".\");\n    const integerPart = parts[0].replace(/[,]/g, \"\");\n    const decimalPart = parts[1] ? parts[1] : \"0\";\n    numericValue = parseFloat(integerPart + \".\" + decimalPart);\n  } else {\n    numericValue = parseFloat(numericPart.replaceAll(\",\", \"\"));\n  }\n\n  let factor = 1;\n\n  switch (multiplier) {\n    case \"k\":\n      factor = 1000;\n      break;\n    case \"m\":\n      factor = 1000000;\n      break;\n    case \"b\":\n      factor = 1000000000;\n      break;\n  }\n\n  return Math.round(numericValue * factor);\n}\n\nfunction convertToDollars(number) {\n  const rawCount = convertToRawCount(number);\n\n  const processed = rawCount * 0.000026;\n  if (processed < 0.1) return processed.toFixed(5);\n  return processed.toFixed(2);\n}\n\nconst globalSelectors = {};\nglobalSelectors.postCounts = `[role=\"group\"][id*=\"id__\"]:only-child`;\nglobalSelectors.articleDate = `[role=\"article\"][aria-labelledby*=\"id__\"][tabindex=\"-1\"] time`;\nglobalSelectors.analyticsLink = \" :not(.dollarBox)>a[href*='/analytics']\";\nglobalSelectors.viewCount =\n  globalSelectors.postCounts + globalSelectors.analyticsLink;\n\nconst innerSelectors = {};\ninnerSelectors.dollarSpot = \"div div:first-child\";\ninnerSelectors.viewSVG = \"div div:first-child svg\";\ninnerSelectors.viewAmount = \"div div:last-child span span span\";\ninnerSelectors.articleViewAmount = \"span div:first-child span span span\";\n\nfunction doWork() {\n  const viewCounts = Array.from(\n    document.querySelectorAll(globalSelectors.viewCount)\n  );\n\n  const articleViewDateSections = document.querySelectorAll(globalSelectors.articleDate);\n\n  if (articleViewDateSections.length) {\n    // the rootDateViewsSection will always be the parent->parent->parent of the last element of the articleDate querySelectorAll result\n    let rootDateViewsSection = articleViewDateSections[articleViewDateSections.length - 1].parentElement.parentElement.parentElement;\n\n    // if there is one child, that means it's an old tweet with no viewcount\n    // if there are more than 4, we already added the paycheck value\n    if (rootDateViewsSection?.children?.length !== 1 && rootDateViewsSection?.children.length < 4) {\n      // clone 2nd and 3rd child of rootDateViewsSection\n      const clonedDateViewSeparator =\n        rootDateViewsSection?.children[1].cloneNode(true);\n      const clonedDateView = rootDateViewsSection?.children[2].cloneNode(true);\n\n      // insert clonedDateViews and clonedDateViewsTwo after the 3rd child we just cloned\n      rootDateViewsSection?.insertBefore(\n        clonedDateViewSeparator,\n        rootDateViewsSection?.children[2].nextSibling\n      );\n      rootDateViewsSection?.insertBefore(\n        clonedDateView,\n        rootDateViewsSection?.children[3].nextSibling\n      );\n\n      // get view count value from 'clonedDateViewsTwo'\n      const viewCountValue = clonedDateView?.querySelector(\n        innerSelectors.articleViewAmount\n      )?.textContent;\n      const dollarAmount = convertToDollars(viewCountValue);\n\n      // replace textContent in cloned clonedDateViews (now 4th child) with converted view count value\n      clonedDateView.querySelector(\n        innerSelectors.articleViewAmount\n      ).textContent = \"$\" + dollarAmount;\n\n      // remove 'views' label\n      clonedDateView.querySelector(`span`).children[1].remove();\n    }\n  }\n\n  for (const view of viewCounts) {\n    // only add the dollar box once\n    if (!view.classList.contains(\"replaced\")) {\n      // make sure we don't touch this one again\n      view.classList.add(\"replaced\");\n\n      // get parent and clone to make dollarBox\n      const parent = view.parentElement;\n      const dollarBox = parent.cloneNode(true);\n      dollarBox.classList.add(\"dollarBox\");\n\n      // insert dollarBox after view count\n      parent.parentElement.insertBefore(dollarBox, parent.nextSibling);\n\n      // remove view count icon\n      const oldIcon = dollarBox.querySelector(innerSelectors.viewSVG);\n      oldIcon?.remove();\n\n      // swap the svg for a dollar sign\n      const dollarSpot = dollarBox.querySelector(innerSelectors.dollarSpot)\n        ?.firstChild?.firstChild;\n      dollarSpot.textContent = \"$\";\n\n      // magic alignment value\n      dollarSpot.style.marginTop = \"-0.6rem\";\n    }\n\n    // get the number of views and calculate & set the dollar amount\n    const dollarBox = view.parentElement.nextSibling.firstChild;\n    const viewCount = view.querySelector(\n      innerSelectors.viewAmount\n    )?.textContent;\n    if (viewCount == undefined) continue;\n    const dollarAmountArea = dollarBox.querySelector(innerSelectors.viewAmount);\n    dollarAmountArea.textContent = convertToDollars(viewCount);\n  }\n}\n\nfunction throttle(func, limit) {\n  let lastFunc;\n  let lastRan;\n  return function () {\n    const context = this;\n    const args = arguments;\n    if (!lastRan) {\n      func.apply(context, args);\n      lastRan = Date.now();\n    } else {\n      clearTimeout(lastFunc);\n      lastFunc = setTimeout(function () {\n        if (Date.now() - lastRan >= limit) {\n          func.apply(context, args);\n          lastRan = Date.now();\n        }\n      }, limit - (Date.now() - lastRan));\n    }\n  };\n}\n\n// Function to start MutationObserver\nconst observe = () => {\n  const runDocumentMutations = throttle(() => {\n    requestAnimationFrame(doWork);\n  }, 1000);\n\n  const observer = new MutationObserver((mutationsList) => {\n    if (!mutationsList.length) return;\n    runDocumentMutations();\n  });\n\n  observer.observe(document, {\n    childList: true,\n    subtree: true,\n  });\n};\n\nobserve();\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"PayCheck for X (Formerly Twitter)\",\n  \"description\": \"See (a very VERY rough idea of) how much money a post is worth\",\n  \"version\": \"0.0.3\",\n  \"icons\": {\n    \"16\": \"/assets/PCX-icon-16.png\",\n    \"32\": \"/assets/PCX-icon-32.png\",\n    \"48\": \"/assets/PCX-icon-48.png\",\n    \"128\": \"/assets/PCX-icon-128.png\"\n  },\n  \"author\": \"Theo\",\n  \"content_scripts\": [\n    {\n      \"run_at\": \"document_end\",\n      \"matches\": [\n        \"https://twitter.com/*\",\n        \"https://mobile.twitter.com/*\",\n        \"https://tweetdeck.twitter.com/*\",\n        \"https://x.com/*\"\n      ],\n      \"js\": [\"main.js\"]\n    }\n  ]\n}\n"
  }
]