Scratch Homepage Improvement

Removes useless stuff from Scratch's homepage.

// ==UserScript==
// @name         Scratch Homepage Improvement
// @namespace    http://tampermonkey.net/
// @version      2025-06-20
// @description  Removes useless stuff from Scratch's homepage.
// @author       Spentine
// @match        https://scratch.mit.edu/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=mit.edu
// @grant        none
// @license      MIT
// ==/UserScript==

(async function() {
  'use strict';
  
  // used in scratch api example (see below)
  const customProjectExample = "802293688"; // example project ID
  
  // options for userscript
  const options = {
    // boxes to remove
    activityBox: false,
    scratchNewsBox: false,
    featuredProjectsBox: true,
    featuredStudiosBox: true,
    scratchDesignStudioBox: true,
    lovedScratchersBox: false,
    communityRemixingBox: true,
    communityLovingBox: true,
    
    // header items to remove
    createButton: false,
    exploreButton: true,
    ideasButton: true,
    aboutButton: true,
    
    // extra homepage elements
    homepageElements: [
      {
        text: "Discuss Scratch",
        type: "headerLink",
        position: 2, // position in the header
        link: "https://scratch.mit.edu/discuss/",
      },
      // {
      //   text: "Discuss Scratch",
      //   type: "buttonLink",
      //   link: "https://scratch.mit.edu/discuss/",
      // },
      {
        text: `Project ${customProjectExample}'s Stats`,
        type: "button",
        function: async function () {
          // perform API call
          const response = await fetch("https://api.scratch.mit.edu/projects/${customProjectExample}");
          
          if (!response.ok) {
            console.error("Failed to fetch project data.");
            return;
          }
          
          const projectData = await response.json();
          const stats = projectData.stats;
          
          const message = (
            `Project ${customProjectExample} Stats:\n\n` +
            `Views: ${stats.views}\n` +
            `Loves: ${stats.loves}\n` +
            `Favorites: ${stats.favorites}\n` +
            `Remixes: ${stats.remixes}\n`
          );
          
          // display the information in an alert
          alert(message);
        },
      },
    ],
  };
  
  // === userscript behavior and code ===
  
  console.log("Scratch Homepage Improvement script loaded.");
  
  // some elements that are important
  const view = document.getElementById("view"); // main view
  const mainScroll = view.firstChild.lastChild; // main scroll element
  const navigation = document.getElementById("navigation"); // header navigation
  const navigationTable = navigation.firstChild.firstChild; // navigation table
  
  function removeBoxes() {
    // box titles
    const boxTitles = {
      activityBox: "What's Happening?",
      scratchNewsBox: "Scratch News",
      featuredProjectsBox: "Featured Projects",
      featuredStudiosBox: "Featured Studios",
      scratchDesignStudioBox: "Scratch Design Studio",
      lovedScratchersBox: "Projects Loved by Scratchers I'm Following",
      communityRemixingBox: "What the Community is Remixing",
      communityLovingBox: "What the Community is Loving",
    };
    const boxNames = Object.keys(boxTitles);
    
    // get the boxes on the homepage
    const boxes = document.getElementsByClassName("box");
    
    // loop through boxes
    for (let boxIndex = 0; boxIndex < boxes.length; boxIndex++) {
      const box = boxes[boxIndex];
      
      // get box title
      const boxTitle = box.firstChild.firstChild.textContent.trim();
      
      // iterate through box names and check if it should be removed
      for (const boxName of boxNames) {
        // skip if the box shouldn't be removed
        if (!options[boxName]) continue;
        
        // check if the box title matches the current box name
        const boxTitleCheck = boxTitles[boxName];
        if (boxTitle.includes(boxTitleCheck)) {
          // remove the box
          box.style.display = "none";
          console.log(`Removed box: ${boxTitleCheck}`);
          break; // exit the loop after removing the box
        }
      }
    }
  }
  
  function removeHeaderButtons() {
    const buttonQueries = {
      createButton: ".link.create",
      exploreButton: ".link.explore",
      ideasButton: ".link.ideas",
      aboutButton: ".link.about",
    };
    const buttonNames = Object.keys(buttonQueries);
    
    // remove header buttons based on options
    for (const buttonName of buttonNames) {
      if (!options[buttonName]) continue; // skip if the button shouldn't be removed
      
      const buttonQuery = buttonQueries[buttonName];
      const button = navigation.querySelector(buttonQuery);
      
      if (button) {
        button.style.display = "none"; // hide the button
        console.log(`Removed header button: ${buttonName}`);
      }
    }
  }
  
  function addElements() {
    const elements = options.homepageElements;
    
    for (const element of elements) {
      if (element.type === "button") {
        // create a button element
        
        const button = document.createElement("button");
        button.className = "custom-button";
        button.textContent = element.text;
        
        // add click event listener
        button.addEventListener("click", element.function);
        
        mainScroll.appendChild(button);
      } else if (element.type === "buttonLink") {
        // create an a element styled as a button
        
        const buttonLink = document.createElement("a");
        buttonLink.className = "custom-button";
        buttonLink.textContent = element.text;
        
        buttonLink.href = element.link;
        
        mainScroll.appendChild(buttonLink);
      } else if (element.type === "headerLink") {
        // create a header link
        
        const headerContainer = document.createElement("li");
        headerContainer.className = "link";
        
        const headerLink = document.createElement("a");
        headerLink.href = element.link;
        
        const innerSpan = document.createElement("span");
        innerSpan.textContent = element.text;
        
        // add hierarchy
        headerLink.appendChild(innerSpan);
        headerContainer.appendChild(headerLink);
        
        // get children of navigation table
        const navChildren = navigationTable.children;
        
        const afterChild = navChildren[element.position];
        console.log(afterChild);
        
        // add the link to the navigation
        navigationTable.insertBefore(headerContainer, afterChild);
      } else {
        console.warn(`Unknown element type: ${element.type}`);
      }
    }
  }
  
  function addCustomCss() {
    const styles = document.createElement("style");
    styles.textContent = `
      .custom-button {
        background-color: hsl(260,60%,60%);
        
        padding: 10px 20px;
        margin: 0px 0px 20px 8px;
        
        border: none;
        border-radius: 4px;
        
        color: white !important;
        font-weight: bold;
      }
    `;
    
    document.head.appendChild(styles);
  }
  
  // initial removal of boxes
  removeBoxes();
  
  // initial removal of header buttons
  // removeHeaderButtons();
  
  // add extra homepage elements
  addElements();
  
  // add custom CSS to make things look better
  addCustomCss();
  
  // observe changes to the homepage and remove boxes if they reappear
  const boxMutationObserver = new MutationObserver(removeBoxes);
  boxMutationObserver.observe(view, {
    childList: true,
    subtree: true
  });
  
  // observe changes to the header and remove buttons if they reappear
  const headerMutationObserver = new MutationObserver(removeHeaderButtons);
  headerMutationObserver.observe(navigation, {
    childList: true,
    subtree: true
  });
})();