From b16dada82ed848b2e3f89f80147702bee21d49de Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Fri, 15 Nov 2024 16:22:23 -0800
Subject: [PATCH 01/39] fix: renamed files, components, imports, exports,
 contexts, and methods

---
 .../{Nav/NavBar.jsx => Menubar/Menubar.jsx}   |  54 ++++----
 .../MenubarItem.jsx}                          |  12 +-
 .../MenubarMenu.jsx}                          |  16 +--
 .../components/{Nav => Menubar}/contexts.jsx  |   6 +-
 .../IDE/components/Header/MobileNav.jsx       |  10 +-
 client/modules/IDE/components/Header/Nav.jsx  | 124 +++++++++---------
 6 files changed, 109 insertions(+), 113 deletions(-)
 rename client/components/{Nav/NavBar.jsx => Menubar/Menubar.jsx} (51%)
 rename client/components/{Nav/NavMenuItem.jsx => Menubar/MenubarItem.jsx} (73%)
 rename client/components/{Nav/NavDropdownMenu.jsx => Menubar/MenubarMenu.jsx} (75%)
 rename client/components/{Nav => Menubar}/contexts.jsx (62%)

diff --git a/client/components/Nav/NavBar.jsx b/client/components/Menubar/Menubar.jsx
similarity index 51%
rename from client/components/Nav/NavBar.jsx
rename to client/components/Menubar/Menubar.jsx
index c8ae7c6377..ebd78b97d5 100644
--- a/client/components/Nav/NavBar.jsx
+++ b/client/components/Menubar/Menubar.jsx
@@ -1,16 +1,16 @@
 import PropTypes from 'prop-types';
 import React, { useCallback, useMemo, useRef, useState } from 'react';
 import useModalClose from '../../common/useModalClose';
-import { MenuOpenContext, NavBarContext } from './contexts';
+import { MenuOpenContext, MenubarContext } from './contexts';
 
-function NavBar({ children, className }) {
-  const [dropdownOpen, setDropdownOpen] = useState('none');
+function Menubar({ children, className }) {
+  const [menuOpen, setMenuOpen] = useState('none');
 
   const timerRef = useRef(null);
 
   const handleClose = useCallback(() => {
-    setDropdownOpen('none');
-  }, [setDropdownOpen]);
+    setMenuOpen('none');
+  }, [setMenuOpen]);
 
   const nodeRef = useModalClose(handleClose);
 
@@ -22,71 +22,67 @@ function NavBar({ children, className }) {
   }, [timerRef]);
 
   const handleBlur = useCallback(() => {
-    timerRef.current = setTimeout(() => setDropdownOpen('none'), 10);
-  }, [timerRef, setDropdownOpen]);
+    timerRef.current = setTimeout(() => setMenuOpen('none'), 10);
+  }, [timerRef, setMenuOpen]);
 
-  const toggleDropdownOpen = useCallback(
-    (dropdown) => {
-      setDropdownOpen((prevState) =>
-        prevState === dropdown ? 'none' : dropdown
-      );
+  const toggleMenuOpen = useCallback(
+    (menu) => {
+      setMenuOpen((prevState) => (prevState === menu ? 'none' : menu));
     },
-    [setDropdownOpen]
+    [setMenuOpen]
   );
 
   const contextValue = useMemo(
     () => ({
-      createDropdownHandlers: (dropdown) => ({
+      createMenuHandlers: (menu) => ({
         onMouseOver: () => {
-          setDropdownOpen((prevState) =>
-            prevState === 'none' ? 'none' : dropdown
-          );
+          setMenuOpen((prevState) => (prevState === 'none' ? 'none' : menu));
         },
         onClick: () => {
-          toggleDropdownOpen(dropdown);
+          toggleMenuOpen(menu);
         },
         onBlur: handleBlur,
         onFocus: clearHideTimeout
       }),
-      createMenuItemHandlers: (dropdown) => ({
+      createMenuItemHandlers: (menu) => ({
         onMouseUp: (e) => {
           if (e.button === 2) {
             return;
           }
-          setDropdownOpen('none');
+          setMenuOpen('none');
         },
         onBlur: handleBlur,
         onFocus: () => {
           clearHideTimeout();
-          setDropdownOpen(dropdown);
+          setMenuOpen(menu);
         }
       }),
-      toggleDropdownOpen
+      toggleMenuOpen
     }),
-    [setDropdownOpen, toggleDropdownOpen, clearHideTimeout, handleBlur]
+    [setMenuOpen, toggleMenuOpen, clearHideTimeout, handleBlur]
   );
 
   return (
-    <NavBarContext.Provider value={contextValue}>
+    <MenubarContext.Provider value={contextValue}>
       <header>
         <div className={className} ref={nodeRef}>
-          <MenuOpenContext.Provider value={dropdownOpen}>
+          <MenuOpenContext.Provider value={menuOpen}>
             {children}
           </MenuOpenContext.Provider>
         </div>
       </header>
-    </NavBarContext.Provider>
+    </MenubarContext.Provider>
   );
 }
 
-NavBar.propTypes = {
+Menubar.propTypes = {
   children: PropTypes.node,
   className: PropTypes.string
 };
 
-NavBar.defaultProps = {
+Menubar.defaultProps = {
   children: null,
   className: 'nav'
 };
 
-export default NavBar;
+export default Menubar;
diff --git a/client/components/Nav/NavMenuItem.jsx b/client/components/Menubar/MenubarItem.jsx
similarity index 73%
rename from client/components/Nav/NavMenuItem.jsx
rename to client/components/Menubar/MenubarItem.jsx
index 09436e43ee..8c08aa9c03 100644
--- a/client/components/Nav/NavMenuItem.jsx
+++ b/client/components/Menubar/MenubarItem.jsx
@@ -1,12 +1,12 @@
 import PropTypes from 'prop-types';
 import React, { useContext, useMemo } from 'react';
 import ButtonOrLink from '../../common/ButtonOrLink';
-import { NavBarContext, ParentMenuContext } from './contexts';
+import { MenubarContext, ParentMenuContext } from './contexts';
 
-function NavMenuItem({ hideIf, className, ...rest }) {
+function MenubarItem({ hideIf, className, ...rest }) {
   const parent = useContext(ParentMenuContext);
 
-  const { createMenuItemHandlers } = useContext(NavBarContext);
+  const { createMenuItemHandlers } = useContext(MenubarContext);
 
   const handlers = useMemo(() => createMenuItemHandlers(parent), [
     createMenuItemHandlers,
@@ -24,7 +24,7 @@ function NavMenuItem({ hideIf, className, ...rest }) {
   );
 }
 
-NavMenuItem.propTypes = {
+MenubarItem.propTypes = {
   ...ButtonOrLink.propTypes,
   onClick: PropTypes.func,
   value: PropTypes.string,
@@ -35,11 +35,11 @@ NavMenuItem.propTypes = {
   className: PropTypes.string
 };
 
-NavMenuItem.defaultProps = {
+MenubarItem.defaultProps = {
   onClick: null,
   value: null,
   hideIf: false,
   className: 'nav__dropdown-item'
 };
 
-export default NavMenuItem;
+export default MenubarItem;
diff --git a/client/components/Nav/NavDropdownMenu.jsx b/client/components/Menubar/MenubarMenu.jsx
similarity index 75%
rename from client/components/Nav/NavDropdownMenu.jsx
rename to client/components/Menubar/MenubarMenu.jsx
index d2c5744c46..4dd47d3fb5 100644
--- a/client/components/Nav/NavDropdownMenu.jsx
+++ b/client/components/Menubar/MenubarMenu.jsx
@@ -2,24 +2,24 @@ import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import React, { useContext, useMemo } from 'react';
 import TriangleIcon from '../../images/down-filled-triangle.svg';
-import { MenuOpenContext, NavBarContext, ParentMenuContext } from './contexts';
+import { MenuOpenContext, MenubarContext, ParentMenuContext } from './contexts';
 
 export function useMenuProps(id) {
   const activeMenu = useContext(MenuOpenContext);
 
   const isOpen = id === activeMenu;
 
-  const { createDropdownHandlers } = useContext(NavBarContext);
+  const { createMenuHandlers } = useContext(MenubarContext);
 
-  const handlers = useMemo(() => createDropdownHandlers(id), [
-    createDropdownHandlers,
+  const handlers = useMemo(() => createMenuHandlers(id), [
+    createMenuHandlers,
     id
   ]);
 
   return { isOpen, handlers };
 }
 
-function NavDropdownMenu({ id, title, children }) {
+function MenubarMenu({ id, title, children }) {
   const { isOpen, handlers } = useMenuProps(id);
 
   return (
@@ -46,14 +46,14 @@ function NavDropdownMenu({ id, title, children }) {
   );
 }
 
-NavDropdownMenu.propTypes = {
+MenubarMenu.propTypes = {
   id: PropTypes.string.isRequired,
   title: PropTypes.node.isRequired,
   children: PropTypes.node
 };
 
-NavDropdownMenu.defaultProps = {
+MenubarMenu.defaultProps = {
   children: null
 };
 
-export default NavDropdownMenu;
+export default MenubarMenu;
diff --git a/client/components/Nav/contexts.jsx b/client/components/Menubar/contexts.jsx
similarity index 62%
rename from client/components/Nav/contexts.jsx
rename to client/components/Menubar/contexts.jsx
index 896d7283f4..ab3bb9ffcf 100644
--- a/client/components/Nav/contexts.jsx
+++ b/client/components/Menubar/contexts.jsx
@@ -4,8 +4,8 @@ export const ParentMenuContext = createContext('none');
 
 export const MenuOpenContext = createContext('none');
 
-export const NavBarContext = createContext({
-  createDropdownHandlers: () => ({}),
+export const MenubarContext = createContext({
+  createMenuHandlers: () => ({}),
   createMenuItemHandlers: () => ({}),
-  toggleDropdownOpen: () => {}
+  toggleMenuOpen: () => {}
 });
diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx
index 37fa16bed3..c1fd718a8f 100644
--- a/client/modules/IDE/components/Header/MobileNav.jsx
+++ b/client/modules/IDE/components/Header/MobileNav.jsx
@@ -5,10 +5,10 @@ import { useTranslation } from 'react-i18next';
 import { Link } from 'react-router-dom';
 import { sortBy } from 'lodash';
 import classNames from 'classnames';
-import { ParentMenuContext } from '../../../../components/Nav/contexts';
-import NavBar from '../../../../components/Nav/NavBar';
-import { useMenuProps } from '../../../../components/Nav/NavDropdownMenu';
-import NavMenuItem from '../../../../components/Nav/NavMenuItem';
+import { ParentMenuContext } from '../../../../components/Menubar/contexts';
+import Menubar from '../../../../components/Menubar/Menubar';
+import { useMenuProps } from '../../../../components/Menubar/MenubarMenu';
+import NavMenuItem from '../../../../components/Menubar/MenubarItem';
 import { prop, remSize } from '../../../../theme';
 import AsteriskIcon from '../../../../images/p5-asterisk.svg';
 import IconButton from '../../../../common/IconButton';
@@ -36,7 +36,7 @@ import Overlay from '../../../App/components/Overlay';
 import ProjectName from './ProjectName';
 import CollectionCreate from '../../../User/components/CollectionCreate';
 
-const Nav = styled(NavBar)`
+const Nav = styled(Menubar)`
   background: ${prop('MobilePanel.default.background')};
   color: ${prop('primaryTextColor')};
   padding: ${remSize(8)} 0;
diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx
index 3492c4388a..dc6b0c6b71 100644
--- a/client/modules/IDE/components/Header/Nav.jsx
+++ b/client/modules/IDE/components/Header/Nav.jsx
@@ -4,13 +4,13 @@ import { sortBy } from 'lodash';
 import { Link } from 'react-router-dom';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
-import NavDropdownMenu from '../../../../components/Nav/NavDropdownMenu';
-import NavMenuItem from '../../../../components/Nav/NavMenuItem';
+import MenubarMenu from '../../../../components/Menubar/MenubarMenu';
+import MenubarItem from '../../../../components/Menubar/MenubarItem';
 import { availableLanguages, languageKeyToLabel } from '../../../../i18n';
 import getConfig from '../../../../utils/getConfig';
 import { showToast } from '../../actions/toast';
 import { setLanguage } from '../../actions/preferences';
-import NavBar from '../../../../components/Nav/NavBar';
+import Menubar from '../../../../components/Menubar/Menubar';
 import CaretLeftIcon from '../../../../images/left-arrow.svg';
 import LogoIcon from '../../../../images/p5js-logo-small.svg';
 import { selectRootFile } from '../../selectors/files';
@@ -37,10 +37,10 @@ const Nav = ({ layout }) => {
   return isMobile ? (
     <MobileNav />
   ) : (
-    <NavBar>
+    <Menubar>
       <LeftLayout layout={layout} />
       <UserMenu />
-    </NavBar>
+    </Menubar>
   );
 };
 
@@ -159,9 +159,9 @@ const ProjectMenu = () => {
           </a>
         )}
       </li>
-      <NavDropdownMenu id="file" title={t('Nav.File.Title')}>
-        <NavMenuItem onClick={newSketch}>{t('Nav.File.New')}</NavMenuItem>
-        <NavMenuItem
+      <MenubarMenu id="file" title={t('Nav.File.Title')}>
+        <MenubarItem onClick={newSketch}>{t('Nav.File.New')}</MenubarItem>
+        <MenubarItem
           hideIf={
             !getConfig('LOGIN_ENABLED') || (project?.owner && !isUserOwner)
           }
@@ -169,26 +169,26 @@ const ProjectMenu = () => {
         >
           {t('Common.Save')}
           <span className="nav__keyboard-shortcut">{metaKeyName}+S</span>
-        </NavMenuItem>
-        <NavMenuItem
+        </MenubarItem>
+        <MenubarItem
           hideIf={isUnsaved || !user.authenticated}
           onClick={() => dispatch(cloneProject())}
         >
           {t('Nav.File.Duplicate')}
-        </NavMenuItem>
-        <NavMenuItem hideIf={isUnsaved} onClick={shareSketch}>
+        </MenubarItem>
+        <MenubarItem hideIf={isUnsaved} onClick={shareSketch}>
           {t('Nav.File.Share')}
-        </NavMenuItem>
-        <NavMenuItem hideIf={isUnsaved} onClick={downloadSketch}>
+        </MenubarItem>
+        <MenubarItem hideIf={isUnsaved} onClick={downloadSketch}>
           {t('Nav.File.Download')}
-        </NavMenuItem>
-        <NavMenuItem
+        </MenubarItem>
+        <MenubarItem
           hideIf={!user.authenticated}
           href={`/${user.username}/sketches`}
         >
           {t('Nav.File.Open')}
-        </NavMenuItem>
-        <NavMenuItem
+        </MenubarItem>
+        <MenubarItem
           hideIf={
             !getConfig('UI_COLLECTIONS_ENABLED') ||
             !user.authenticated ||
@@ -197,56 +197,56 @@ const ProjectMenu = () => {
           href={`/${user.username}/sketches/${project?.id}/add-to-collection`}
         >
           {t('Nav.File.AddToCollection')}
-        </NavMenuItem>
-        <NavMenuItem
+        </MenubarItem>
+        <MenubarItem
           hideIf={!getConfig('EXAMPLES_ENABLED')}
           href="/p5/sketches"
         >
           {t('Nav.File.Examples')}
-        </NavMenuItem>
-      </NavDropdownMenu>
-      <NavDropdownMenu id="edit" title={t('Nav.Edit.Title')}>
-        <NavMenuItem onClick={cmRef.current?.tidyCode}>
+        </MenubarItem>
+      </MenubarMenu>
+      <MenubarMenu id="edit" title={t('Nav.Edit.Title')}>
+        <MenubarItem onClick={cmRef.current?.tidyCode}>
           {t('Nav.Edit.TidyCode')}
           <span className="nav__keyboard-shortcut">{metaKeyName}+Shift+F</span>
-        </NavMenuItem>
-        <NavMenuItem onClick={cmRef.current?.showFind}>
+        </MenubarItem>
+        <MenubarItem onClick={cmRef.current?.showFind}>
           {t('Nav.Edit.Find')}
           <span className="nav__keyboard-shortcut">{metaKeyName}+F</span>
-        </NavMenuItem>
-        <NavMenuItem onClick={cmRef.current?.showReplace}>
+        </MenubarItem>
+        <MenubarItem onClick={cmRef.current?.showReplace}>
           {t('Nav.Edit.Replace')}
           <span className="nav__keyboard-shortcut">{replaceCommand}</span>
-        </NavMenuItem>
-      </NavDropdownMenu>
-      <NavDropdownMenu id="sketch" title={t('Nav.Sketch.Title')}>
-        <NavMenuItem onClick={() => dispatch(newFile(rootFile.id))}>
+        </MenubarItem>
+      </MenubarMenu>
+      <MenubarMenu id="sketch" title={t('Nav.Sketch.Title')}>
+        <MenubarItem onClick={() => dispatch(newFile(rootFile.id))}>
           {t('Nav.Sketch.AddFile')}
           <span className="nav__keyboard-shortcut">{newFileCommand}</span>
-        </NavMenuItem>
-        <NavMenuItem onClick={() => dispatch(newFolder(rootFile.id))}>
+        </MenubarItem>
+        <MenubarItem onClick={() => dispatch(newFolder(rootFile.id))}>
           {t('Nav.Sketch.AddFolder')}
-        </NavMenuItem>
-        <NavMenuItem onClick={() => dispatch(startSketch())}>
+        </MenubarItem>
+        <MenubarItem onClick={() => dispatch(startSketch())}>
           {t('Nav.Sketch.Run')}
           <span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span>
-        </NavMenuItem>
-        <NavMenuItem onClick={() => dispatch(stopSketch())}>
+        </MenubarItem>
+        <MenubarItem onClick={() => dispatch(stopSketch())}>
           {t('Nav.Sketch.Stop')}
           <span className="nav__keyboard-shortcut">
             Shift+{metaKeyName}+Enter
           </span>
-        </NavMenuItem>
-      </NavDropdownMenu>
-      <NavDropdownMenu id="help" title={t('Nav.Help.Title')}>
-        <NavMenuItem onClick={() => dispatch(showKeyboardShortcutModal())}>
+        </MenubarItem>
+      </MenubarMenu>
+      <MenubarMenu id="help" title={t('Nav.Help.Title')}>
+        <MenubarItem onClick={() => dispatch(showKeyboardShortcutModal())}>
           {t('Nav.Help.KeyboardShortcuts')}
-        </NavMenuItem>
-        <NavMenuItem href="https://p5js.org/reference/">
+        </MenubarItem>
+        <MenubarItem href="https://p5js.org/reference/">
           {t('Nav.Help.Reference')}
-        </NavMenuItem>
-        <NavMenuItem href="/about">{t('Nav.Help.About')}</NavMenuItem>
-      </NavDropdownMenu>
+        </MenubarItem>
+        <MenubarItem href="/about">{t('Nav.Help.About')}</MenubarItem>
+      </MenubarMenu>
     </ul>
   );
 };
@@ -261,14 +261,14 @@ const LanguageMenu = () => {
   }
 
   return (
-    <NavDropdownMenu id="lang" title={languageKeyToLabel(language)}>
+    <MenubarMenu id="lang" title={languageKeyToLabel(language)}>
       {sortBy(availableLanguages).map((key) => (
         // eslint-disable-next-line react/jsx-no-bind
-        <NavMenuItem key={key} value={key} onClick={handleLangSelection}>
+        <MenubarItem key={key} value={key} onClick={handleLangSelection}>
           {languageKeyToLabel(key)}
-        </NavMenuItem>
+        </MenubarItem>
       ))}
-    </NavDropdownMenu>
+    </MenubarMenu>
   );
 };
 
@@ -305,7 +305,7 @@ const AuthenticatedUserMenu = () => {
   return (
     <ul className="nav__items-right" title="user-menu" role="navigation">
       {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
-      <NavDropdownMenu
+      <MenubarMenu
         id="account"
         title={
           <span>
@@ -313,23 +313,23 @@ const AuthenticatedUserMenu = () => {
           </span>
         }
       >
-        <NavMenuItem href={`/${username}/sketches`}>
+        <MenubarItem href={`/${username}/sketches`}>
           {t('Nav.Auth.MySketches')}
-        </NavMenuItem>
-        <NavMenuItem
+        </MenubarItem>
+        <MenubarItem
           href={`/${username}/collections`}
           hideIf={!getConfig('UI_COLLECTIONS_ENABLED')}
         >
           {t('Nav.Auth.MyCollections')}
-        </NavMenuItem>
-        <NavMenuItem href={`/${username}/assets`}>
+        </MenubarItem>
+        <MenubarItem href={`/${username}/assets`}>
           {t('Nav.Auth.MyAssets')}
-        </NavMenuItem>
-        <NavMenuItem href="/account">{t('Preferences.Settings')}</NavMenuItem>
-        <NavMenuItem onClick={() => dispatch(logoutUser())}>
+        </MenubarItem>
+        <MenubarItem href="/account">{t('Preferences.Settings')}</MenubarItem>
+        <MenubarItem onClick={() => dispatch(logoutUser())}>
           {t('Nav.Auth.LogOut')}
-        </NavMenuItem>
-      </NavDropdownMenu>
+        </MenubarItem>
+      </MenubarMenu>
     </ul>
   );
 };

From 0f9cf65c9c050ebed8c86bd9070f41ac1b29a3f0 Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Mon, 18 Nov 2024 14:05:14 -0800
Subject: [PATCH 02/39] refactor: separated UserMenu from the menubar
 component, wrapped both in a header container, removed navigation role from
 UserMenu

---
 client/components/Menubar/Menubar.jsx        | 12 +++++-------
 client/modules/IDE/components/Header/Nav.jsx | 16 ++++++++++------
 2 files changed, 15 insertions(+), 13 deletions(-)

diff --git a/client/components/Menubar/Menubar.jsx b/client/components/Menubar/Menubar.jsx
index ebd78b97d5..b806246515 100644
--- a/client/components/Menubar/Menubar.jsx
+++ b/client/components/Menubar/Menubar.jsx
@@ -64,13 +64,11 @@ function Menubar({ children, className }) {
 
   return (
     <MenubarContext.Provider value={contextValue}>
-      <header>
-        <div className={className} ref={nodeRef}>
-          <MenuOpenContext.Provider value={menuOpen}>
-            {children}
-          </MenuOpenContext.Provider>
-        </div>
-      </header>
+      <div className={className} ref={nodeRef}>
+        <MenuOpenContext.Provider value={menuOpen}>
+          {children}
+        </MenuOpenContext.Provider>
+      </div>
     </MenubarContext.Provider>
   );
 }
diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx
index dc6b0c6b71..0e5f2cdac7 100644
--- a/client/modules/IDE/components/Header/Nav.jsx
+++ b/client/modules/IDE/components/Header/Nav.jsx
@@ -37,10 +37,14 @@ const Nav = ({ layout }) => {
   return isMobile ? (
     <MobileNav />
   ) : (
-    <Menubar>
-      <LeftLayout layout={layout} />
-      <UserMenu />
-    </Menubar>
+    <>
+      <header className="nav__header">
+        <Menubar>
+          <LeftLayout layout={layout} />
+        </Menubar>
+        <UserMenu />
+      </header>
+    </>
   );
 };
 
@@ -275,7 +279,7 @@ const LanguageMenu = () => {
 const UnauthenticatedUserMenu = () => {
   const { t } = useTranslation();
   return (
-    <ul className="nav__items-right" title="user-menu" role="navigation">
+    <ul className="nav__items-right" title="user-menu">
       {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
       <li className="nav__item">
         <Link to="/login" className="nav__auth-button" role="menuitem">
@@ -303,7 +307,7 @@ const AuthenticatedUserMenu = () => {
   const dispatch = useDispatch();
 
   return (
-    <ul className="nav__items-right" title="user-menu" role="navigation">
+    <ul className="nav__items-right" title="user-menu">
       {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
       <MenubarMenu
         id="account"

From ab818ad0cb5ed1ad8c3f39cb3fa3ae62897a47db Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Mon, 18 Nov 2024 14:07:06 -0800
Subject: [PATCH 03/39] style: updated header and menubar styles

---
 client/styles/components/_nav.scss | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss
index 1f4a7fc364..4ddc96b327 100644
--- a/client/styles/components/_nav.scss
+++ b/client/styles/components/_nav.scss
@@ -6,23 +6,31 @@
   flex-direction: row;
   justify-content: space-between;
 
-  @include themify() {
-    border-bottom: 1px dashed map-get($theme-map, 'nav-border-color');
-  }
-
   & button {
     padding: 0;
   }
 }
 
+.nav__header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  height: 100%;
+  align-items: center;
+
+  @include themify() {
+    border-bottom: 1px dashed map-get($theme-map, 'nav-border-color');
+  }
+  // padding-left: #{math.div(20, $base-font-size)}rem;
+}
+
 .nav__items-left,
 .nav__items-right {
   list-style: none;
   display: flex;
   flex-direction: row;
-  justify-content: flex-end;
-  height: 100%;
   align-items: center;
+  height: 100%;
 }
 
 .preview-nav__editor-svg {

From 246597f9442dfc70d83a80125d740fb05c6a503e Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Mon, 18 Nov 2024 15:52:26 -0800
Subject: [PATCH 04/39] refactor: moved LanguageMenu into left items

---
 client/modules/IDE/components/Header/Nav.jsx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx
index 0e5f2cdac7..6721209761 100644
--- a/client/modules/IDE/components/Header/Nav.jsx
+++ b/client/modules/IDE/components/Header/Nav.jsx
@@ -251,6 +251,7 @@ const ProjectMenu = () => {
         </MenubarItem>
         <MenubarItem href="/about">{t('Nav.Help.About')}</MenubarItem>
       </MenubarMenu>
+      {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
     </ul>
   );
 };
@@ -280,7 +281,6 @@ const UnauthenticatedUserMenu = () => {
   const { t } = useTranslation();
   return (
     <ul className="nav__items-right" title="user-menu">
-      {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
       <li className="nav__item">
         <Link to="/login" className="nav__auth-button" role="menuitem">
           <span className="nav__item-header" title="Login">
@@ -308,7 +308,6 @@ const AuthenticatedUserMenu = () => {
 
   return (
     <ul className="nav__items-right" title="user-menu">
-      {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
       <MenubarMenu
         id="account"
         title={

From 9f3096cf7f905e0582cfb6e16f831f59e94155cd Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Mon, 18 Nov 2024 16:06:30 -0800
Subject: [PATCH 05/39] refactor: split new MenubarMenu component into Trigger,
 List, and Menu components

---
 client/components/Menubar/MenubarMenu.jsx | 65 ++++++++++++++++-------
 1 file changed, 47 insertions(+), 18 deletions(-)

diff --git a/client/components/Menubar/MenubarMenu.jsx b/client/components/Menubar/MenubarMenu.jsx
index 4dd47d3fb5..5453e6c8d8 100644
--- a/client/components/Menubar/MenubarMenu.jsx
+++ b/client/components/Menubar/MenubarMenu.jsx
@@ -19,29 +19,58 @@ export function useMenuProps(id) {
   return { isOpen, handlers };
 }
 
+function MenubarTrigger({ id, title, ...props }) {
+  const { isOpen, handlers } = useMenuProps(id);
+
+  return (
+    <button
+      {...handlers}
+      {...props}
+      role="menuitem"
+      aria-haspopup="menu"
+      aria-expanded={isOpen}
+    >
+      <span className="nav__item-header">{title}</span>
+      <TriangleIcon
+        className="nav__item-header-triangle"
+        focusable="false"
+        aria-hidden="true"
+      />
+    </button>
+  );
+}
+
+MenubarTrigger.propTypes = {
+  id: PropTypes.string.isRequired,
+  title: PropTypes.node.isRequired
+};
+
+function MenubarList({ id, children }) {
+  return (
+    <ul className="nav__dropdown" role="menu">
+      <ParentMenuContext.Provider value={id}>
+        {children}
+      </ParentMenuContext.Provider>
+    </ul>
+  );
+}
+
+MenubarList.propTypes = {
+  id: PropTypes.string.isRequired,
+  children: PropTypes.node
+};
+
+MenubarList.defaultProps = {
+  children: null
+};
+
 function MenubarMenu({ id, title, children }) {
   const { isOpen, handlers } = useMenuProps(id);
 
   return (
     <li className={classNames('nav__item', isOpen && 'nav__item--open')}>
-      <button
-        {...handlers}
-        role="menuitem"
-        aria-haspopup="menu"
-        aria-expanded={isOpen}
-      >
-        <span className="nav__item-header">{title}</span>
-        <TriangleIcon
-          className="nav__item-header-triangle"
-          focusable="false"
-          aria-hidden="true"
-        />
-      </button>
-      <ul className="nav__dropdown" role="menu">
-        <ParentMenuContext.Provider value={id}>
-          {children}
-        </ParentMenuContext.Provider>
-      </ul>
+      <MenubarTrigger id={id} title={title} />
+      <MenubarList id={id}>{children}</MenubarList>
     </li>
   );
 }

From 7c3b896800f5828cf99eaa02565b760736887417 Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Mon, 18 Nov 2024 16:08:52 -0800
Subject: [PATCH 06/39] test: updated snapshots

---
 .../__snapshots__/Nav.unit.test.jsx.snap      | 732 +++++++++---------
 1 file changed, 366 insertions(+), 366 deletions(-)

diff --git a/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap b/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap
index af56a1a418..d9140653c1 100644
--- a/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap
+++ b/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap
@@ -2,7 +2,9 @@
 
 exports[`Nav renders dashboard version for desktop 1`] = `
 <DocumentFragment>
-  <header>
+  <header
+    class="nav__header"
+  >
     <div
       class="nav"
     >
@@ -265,71 +267,54 @@ exports[`Nav renders dashboard version for mobile 1`] = `
   color: #FFF;
 }
 
-<header>
+<div
+    class="c0"
+  >
     <div
-      class="c0"
+      class="c1"
     >
-      <div
-        class="c1"
-      >
-        <test-file-stub />
-      </div>
-      <div
-        class="c2"
-      >
-        <h1>
-          <span
-            class="editable-input editable-input--is-not-editing editable-input--has-value "
+      <test-file-stub />
+    </div>
+    <div
+      class="c2"
+    >
+      <h1>
+        <span
+          class="editable-input editable-input--is-not-editing editable-input--has-value "
+        >
+          <button
+            aria-hidden="false"
+            aria-label="Edit sketch name"
+            class="editable-input__label"
           >
-            <button
-              aria-hidden="false"
-              aria-label="Edit sketch name"
-              class="editable-input__label"
-            >
-              <span>
-                Test project name
-              </span>
-              <test-file-stub
-                aria-hidden="true"
-                classname="editable-input__icon"
-                focusable="false"
-              />
-            </button>
-            <input
+            <span>
+              Test project name
+            </span>
+            <test-file-stub
               aria-hidden="true"
-              aria-label="New sketch name"
-              class="editable-input__input"
-              disabled=""
-              maxlength="128"
-              type="text"
-              value="Test project name"
-            />
-          </span>
-        </h1>
-      </div>
-      <div
-        class="c3"
-      >
-        <div>
-          <a
-            href="/login"
-          >
-            <button
-              class="c4 c5"
-              display="inline"
+              classname="editable-input__icon"
               focusable="false"
-              kind="primary"
-              type="button"
-            >
-              <test-file-stub
-                aria-hidden="true"
-                classname="sc-fujyAs cSTVlM"
-                focusable="false"
-              />
-            </button>
-          </a>
-        </div>
-        <div>
+            />
+          </button>
+          <input
+            aria-hidden="true"
+            aria-label="New sketch name"
+            class="editable-input__input"
+            disabled=""
+            maxlength="128"
+            type="text"
+            value="Test project name"
+          />
+        </span>
+      </h1>
+    </div>
+    <div
+      class="c3"
+    >
+      <div>
+        <a
+          href="/login"
+        >
           <button
             class="c4 c5"
             display="inline"
@@ -339,152 +324,169 @@ exports[`Nav renders dashboard version for mobile 1`] = `
           >
             <test-file-stub
               aria-hidden="true"
-              classname="sc-iqAclL iOZiVo"
+              classname="sc-fujyAs cSTVlM"
               focusable="false"
             />
           </button>
-          <ul
-            class=""
+        </a>
+      </div>
+      <div>
+        <button
+          class="c4 c5"
+          display="inline"
+          focusable="false"
+          kind="primary"
+          type="button"
+        >
+          <test-file-stub
+            aria-hidden="true"
+            classname="sc-iqAclL iOZiVo"
+            focusable="false"
+          />
+        </button>
+        <ul
+          class=""
+        >
+          <b>
+            File
+          </b>
+          <li
+            class="nav__dropdown-item"
           >
-            <b>
-              File
-            </b>
-            <li
-              class="nav__dropdown-item"
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                New
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              New
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Save
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Save
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <a
+              href="/p5/sketches"
+              role="menuitem"
             >
-              <a
-                href="/p5/sketches"
-                role="menuitem"
-              >
-                Examples
-              </a>
-            </li>
-            <b>
-              Edit
-            </b>
-            <li
-              class="nav__dropdown-item"
+              Examples
+            </a>
+          </li>
+          <b>
+            Edit
+          </b>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Tidy Code
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Tidy Code
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Find
-              </button>
-            </li>
-            <b>
-              Sketch
-            </b>
-            <li
-              class="nav__dropdown-item"
+              Find
+            </button>
+          </li>
+          <b>
+            Sketch
+          </b>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Add File
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Add File
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Add Folder
-              </button>
-            </li>
-            <b>
-              Settings
-            </b>
-            <li
-              class="nav__dropdown-item"
+              Add Folder
+            </button>
+          </li>
+          <b>
+            Settings
+          </b>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Preferences
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Preferences
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Language
-              </button>
-            </li>
-            <b>
-              Help
-            </b>
-            <li
-              class="nav__dropdown-item"
+              Language
+            </button>
+          </li>
+          <b>
+            Help
+          </b>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Keyboard Shortcuts
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Keyboard Shortcuts
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <a
+              href="https://p5js.org/reference/"
+              rel="noopener noreferrer"
+              role="menuitem"
+              target="_blank"
             >
-              <a
-                href="https://p5js.org/reference/"
-                rel="noopener noreferrer"
-                role="menuitem"
-                target="_blank"
-              >
-                Reference
-              </a>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Reference
+            </a>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <a
+              href="/about"
+              role="menuitem"
             >
-              <a
-                href="/about"
-                role="menuitem"
-              >
-                About
-              </a>
-            </li>
-          </ul>
-        </div>
+              About
+            </a>
+          </li>
+        </ul>
       </div>
     </div>
-  </header>
+  </div>
 </DocumentFragment>
 `;
 
 exports[`Nav renders editor version for desktop 1`] = `
 <DocumentFragment>
-  <header>
+  <header
+    class="nav__header"
+  >
     <div
       class="nav"
     >
@@ -966,71 +968,54 @@ exports[`Nav renders editor version for mobile 1`] = `
   color: #FFF;
 }
 
-<header>
+<div
+    class="c0"
+  >
     <div
-      class="c0"
+      class="c1"
     >
-      <div
-        class="c1"
-      >
-        <test-file-stub />
-      </div>
-      <div
-        class="c2"
-      >
-        <h1>
-          <span
-            class="editable-input editable-input--is-not-editing editable-input--has-value "
+      <test-file-stub />
+    </div>
+    <div
+      class="c2"
+    >
+      <h1>
+        <span
+          class="editable-input editable-input--is-not-editing editable-input--has-value "
+        >
+          <button
+            aria-hidden="false"
+            aria-label="Edit sketch name"
+            class="editable-input__label"
           >
-            <button
-              aria-hidden="false"
-              aria-label="Edit sketch name"
-              class="editable-input__label"
-            >
-              <span>
-                Test project name
-              </span>
-              <test-file-stub
-                aria-hidden="true"
-                classname="editable-input__icon"
-                focusable="false"
-              />
-            </button>
-            <input
+            <span>
+              Test project name
+            </span>
+            <test-file-stub
               aria-hidden="true"
-              aria-label="New sketch name"
-              class="editable-input__input"
-              disabled=""
-              maxlength="128"
-              type="text"
-              value="Test project name"
-            />
-          </span>
-        </h1>
-      </div>
-      <div
-        class="c3"
-      >
-        <div>
-          <a
-            href="/login"
-          >
-            <button
-              class="c4 c5"
-              display="inline"
+              classname="editable-input__icon"
               focusable="false"
-              kind="primary"
-              type="button"
-            >
-              <test-file-stub
-                aria-hidden="true"
-                classname="sc-fujyAs cSTVlM"
-                focusable="false"
-              />
-            </button>
-          </a>
-        </div>
-        <div>
+            />
+          </button>
+          <input
+            aria-hidden="true"
+            aria-label="New sketch name"
+            class="editable-input__input"
+            disabled=""
+            maxlength="128"
+            type="text"
+            value="Test project name"
+          />
+        </span>
+      </h1>
+    </div>
+    <div
+      class="c3"
+    >
+      <div>
+        <a
+          href="/login"
+        >
           <button
             class="c4 c5"
             display="inline"
@@ -1040,145 +1025,160 @@ exports[`Nav renders editor version for mobile 1`] = `
           >
             <test-file-stub
               aria-hidden="true"
-              classname="sc-iqAclL iOZiVo"
+              classname="sc-fujyAs cSTVlM"
               focusable="false"
             />
           </button>
-          <ul
-            class=""
+        </a>
+      </div>
+      <div>
+        <button
+          class="c4 c5"
+          display="inline"
+          focusable="false"
+          kind="primary"
+          type="button"
+        >
+          <test-file-stub
+            aria-hidden="true"
+            classname="sc-iqAclL iOZiVo"
+            focusable="false"
+          />
+        </button>
+        <ul
+          class=""
+        >
+          <b>
+            File
+          </b>
+          <li
+            class="nav__dropdown-item"
           >
-            <b>
-              File
-            </b>
-            <li
-              class="nav__dropdown-item"
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                New
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              New
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Save
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Save
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <a
+              href="/p5/sketches"
+              role="menuitem"
             >
-              <a
-                href="/p5/sketches"
-                role="menuitem"
-              >
-                Examples
-              </a>
-            </li>
-            <b>
-              Edit
-            </b>
-            <li
-              class="nav__dropdown-item"
+              Examples
+            </a>
+          </li>
+          <b>
+            Edit
+          </b>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Tidy Code
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Tidy Code
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Find
-              </button>
-            </li>
-            <b>
-              Sketch
-            </b>
-            <li
-              class="nav__dropdown-item"
+              Find
+            </button>
+          </li>
+          <b>
+            Sketch
+          </b>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Add File
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Add File
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Add Folder
-              </button>
-            </li>
-            <b>
-              Settings
-            </b>
-            <li
-              class="nav__dropdown-item"
+              Add Folder
+            </button>
+          </li>
+          <b>
+            Settings
+          </b>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Preferences
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Preferences
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Language
-              </button>
-            </li>
-            <b>
-              Help
-            </b>
-            <li
-              class="nav__dropdown-item"
+              Language
+            </button>
+          </li>
+          <b>
+            Help
+          </b>
+          <li
+            class="nav__dropdown-item"
+          >
+            <button
+              role="menuitem"
             >
-              <button
-                role="menuitem"
-              >
-                Keyboard Shortcuts
-              </button>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Keyboard Shortcuts
+            </button>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <a
+              href="https://p5js.org/reference/"
+              rel="noopener noreferrer"
+              role="menuitem"
+              target="_blank"
             >
-              <a
-                href="https://p5js.org/reference/"
-                rel="noopener noreferrer"
-                role="menuitem"
-                target="_blank"
-              >
-                Reference
-              </a>
-            </li>
-            <li
-              class="nav__dropdown-item"
+              Reference
+            </a>
+          </li>
+          <li
+            class="nav__dropdown-item"
+          >
+            <a
+              href="/about"
+              role="menuitem"
             >
-              <a
-                href="/about"
-                role="menuitem"
-              >
-                About
-              </a>
-            </li>
-          </ul>
-        </div>
+              About
+            </a>
+          </li>
+        </ul>
       </div>
     </div>
-  </header>
+  </div>
 </DocumentFragment>
 `;

From b2eb033a463be90db1aba6daaa503d3e32094dfc Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Tue, 19 Nov 2024 10:06:35 -0800
Subject: [PATCH 07/39] refactor: removed menuitem roles from sign up and
 login, added role=presentation to or

---
 client/modules/IDE/components/Header/Nav.jsx | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx
index 6721209761..6ddc38bda2 100644
--- a/client/modules/IDE/components/Header/Nav.jsx
+++ b/client/modules/IDE/components/Header/Nav.jsx
@@ -282,15 +282,17 @@ const UnauthenticatedUserMenu = () => {
   return (
     <ul className="nav__items-right" title="user-menu">
       <li className="nav__item">
-        <Link to="/login" className="nav__auth-button" role="menuitem">
+        <Link to="/login" className="nav__auth-button">
           <span className="nav__item-header" title="Login">
             {t('Nav.Login')}
           </span>
         </Link>
       </li>
-      <li className="nav__item-or">{t('Nav.LoginOr')}</li>
+      <li className="nav__item-or" role="presentation">
+        {t('Nav.LoginOr')}
+      </li>
       <li className="nav__item">
-        <Link to="/signup" className="nav__auth-button" role="menuitem">
+        <Link to="/signup" className="nav__auth-button">
           <span className="nav__item-header" title="SignUp">
             {t('Nav.SignUp')}
           </span>

From 06e915277cbb3471cb9c2523c2853c43a0b6637c Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Tue, 19 Nov 2024 10:08:02 -0800
Subject: [PATCH 08/39] fix: lint fixes

---
 client/components/Menubar/MenubarMenu.jsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/components/Menubar/MenubarMenu.jsx b/client/components/Menubar/MenubarMenu.jsx
index 5453e6c8d8..67c7eb91e3 100644
--- a/client/components/Menubar/MenubarMenu.jsx
+++ b/client/components/Menubar/MenubarMenu.jsx
@@ -65,7 +65,7 @@ MenubarList.defaultProps = {
 };
 
 function MenubarMenu({ id, title, children }) {
-  const { isOpen, handlers } = useMenuProps(id);
+  const { isOpen } = useMenuProps(id);
 
   return (
     <li className={classNames('nav__item', isOpen && 'nav__item--open')}>

From 905ac20605f183b91779c638e59a59fc757219c8 Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Tue, 19 Nov 2024 10:18:19 -0800
Subject: [PATCH 09/39] chore: added separators between components within
 MenubarMenu

---
 client/components/Menubar/MenubarMenu.jsx | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/client/components/Menubar/MenubarMenu.jsx b/client/components/Menubar/MenubarMenu.jsx
index 67c7eb91e3..34192b237b 100644
--- a/client/components/Menubar/MenubarMenu.jsx
+++ b/client/components/Menubar/MenubarMenu.jsx
@@ -19,6 +19,10 @@ export function useMenuProps(id) {
   return { isOpen, handlers };
 }
 
+/* -------------------------------------------------------------------------------------------------
+ * MenubarTrigger
+ * -----------------------------------------------------------------------------------------------*/
+
 function MenubarTrigger({ id, title, ...props }) {
   const { isOpen, handlers } = useMenuProps(id);
 
@@ -45,6 +49,10 @@ MenubarTrigger.propTypes = {
   title: PropTypes.node.isRequired
 };
 
+/* -------------------------------------------------------------------------------------------------
+ * MenubarList
+ * -----------------------------------------------------------------------------------------------*/
+
 function MenubarList({ id, children }) {
   return (
     <ul className="nav__dropdown" role="menu">
@@ -64,6 +72,10 @@ MenubarList.defaultProps = {
   children: null
 };
 
+/* -------------------------------------------------------------------------------------------------
+ * MenubarMenu
+ * -----------------------------------------------------------------------------------------------*/
+
 function MenubarMenu({ id, title, children }) {
   const { isOpen } = useMenuProps(id);
 

From b8a45d5ba22f14f961e0ae7938606e6e8140fdf9 Mon Sep 17 00:00:00 2001
From: Pratyush Raj <pratyushraj@AGLS-MLT-536.local>
Date: Sat, 2 Nov 2024 19:42:32 +0530
Subject: [PATCH 10/39] Fix#3241 Icon Alligment issue in Password Field

---
 client/styles/components/_forms.scss | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/client/styles/components/_forms.scss b/client/styles/components/_forms.scss
index d1237935cc..cf2d8df31f 100644
--- a/client/styles/components/_forms.scss
+++ b/client/styles/components/_forms.scss
@@ -75,13 +75,15 @@
 
 .form__field__password {
   position: relative;
+  display: flex;
+  align-items: center;
 }
 
 .form__eye__icon {
   font-size: 28px;
   position: absolute;
   right: 0px;
-  top: 4px;
+  display: flex;
   vertical-align: middle;
 }
 

From dbc98dd28c6fb0916434102dd360c4b858457dfc Mon Sep 17 00:00:00 2001
From: pratyushsawan <dev@pratyushsawan.co.in>
Date: Sat, 23 Nov 2024 21:42:57 +0530
Subject: [PATCH 11/39] added some fixes on the eye-svg

---
 client/styles/components/_forms.scss | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/client/styles/components/_forms.scss b/client/styles/components/_forms.scss
index cf2d8df31f..daf5fa0d08 100644
--- a/client/styles/components/_forms.scss
+++ b/client/styles/components/_forms.scss
@@ -80,11 +80,13 @@
 }
 
 .form__eye__icon {
-  font-size: 28px;
+  font-size: #{math.div(30, $base-font-size)}rem;
   position: absolute;
   right: 0px;
-  display: flex;
-  vertical-align: middle;
+  
+  & svg {
+    transform: translateY(10%);
+  }
 }
 
 

From 72daf4d1ded685bf38613efa8836e8a9b7231b6b Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Tue, 26 Nov 2024 12:25:51 -0500
Subject: [PATCH 12/39] missing dispatch import

---
 client/modules/IDE/components/ConsoleInput.jsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/client/modules/IDE/components/ConsoleInput.jsx b/client/modules/IDE/components/ConsoleInput.jsx
index c026d29d4b..daac76839c 100644
--- a/client/modules/IDE/components/ConsoleInput.jsx
+++ b/client/modules/IDE/components/ConsoleInput.jsx
@@ -1,5 +1,6 @@
 import PropTypes from 'prop-types';
 import React, { useRef, useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
 import CodeMirror from 'codemirror';
 import { Encode } from 'console-feed';
 
@@ -15,6 +16,7 @@ function ConsoleInput({ theme, fontSize }) {
   const [commandCursor, setCommandCursor] = useState(-1);
   const codemirrorContainer = useRef(null);
   const cmInstance = useRef(null);
+  const dispatch = useDispatch();
 
   useEffect(() => {
     cmInstance.current = CodeMirror(codemirrorContainer.current, {
@@ -45,7 +47,7 @@ function ConsoleInput({ theme, fontSize }) {
           payload: { source: 'console', messages }
         });
 
-        dispatchConsoleEvent(consoleEvent);
+        dispatch(dispatchConsoleEvent(consoleEvent));
         cm.setValue('');
         setCommandHistory([value, ...commandHistory]);
         setCommandCursor(-1);

From e5e2ec12dbea69bc79d93c3b152adbe54169d141 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 27 Nov 2024 17:04:40 -0500
Subject: [PATCH 13/39] wrap actions with dispatch

---
 client/modules/IDE/components/FileNode.jsx | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx
index e589fb3cff..04d7223bdb 100644
--- a/client/modules/IDE/components/FileNode.jsx
+++ b/client/modules/IDE/components/FileNode.jsx
@@ -1,7 +1,7 @@
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import React, { useState, useRef } from 'react';
-import { connect } from 'react-redux';
+import { connect, useDispatch } from 'react-redux';
 import { useTranslation } from 'react-i18next';
 
 import * as IDEActions from '../actions/ide';
@@ -87,6 +87,7 @@ const FileNode = ({
   const [isEditingName, setIsEditingName] = useState(false);
   const [isDeleting, setIsDeleting] = useState(false);
   const [updatedName, setUpdatedName] = useState(name);
+  const dispatch = useDispatch();
 
   const { t } = useTranslation();
   const fileNameInput = useRef(null);
@@ -122,17 +123,17 @@ const FileNode = ({
   };
 
   const handleClickAddFile = () => {
-    newFile(id);
+    dispatch(newFile(id));
     setTimeout(() => hideFileOptions(), 0);
   };
 
   const handleClickAddFolder = () => {
-    newFolder(id);
+    dispatch(newFolder(id));
     setTimeout(() => hideFileOptions(), 0);
   };
 
   const handleClickUploadFile = () => {
-    openUploadFileModal(id);
+    dispatch(openUploadFileModal(id));
     setTimeout(hideFileOptions, 0);
   };
 

From f122ec62d0b5a337d46578c2596f0a84a951c5e0 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 27 Nov 2024 18:46:21 -0500
Subject: [PATCH 14/39] update test to add useDispatch

---
 .../IDE/components/FileNode.unit.test.jsx     | 64 ++++++++++++++-----
 1 file changed, 48 insertions(+), 16 deletions(-)

diff --git a/client/modules/IDE/components/FileNode.unit.test.jsx b/client/modules/IDE/components/FileNode.unit.test.jsx
index 8676a817d8..86d3504626 100644
--- a/client/modules/IDE/components/FileNode.unit.test.jsx
+++ b/client/modules/IDE/components/FileNode.unit.test.jsx
@@ -1,4 +1,7 @@
 import React from 'react';
+import { Provider } from 'react-redux';
+import configureStore from 'redux-mock-store';
+import { useDispatch } from 'react-redux';
 
 import {
   fireEvent,
@@ -9,7 +12,19 @@ import {
 } from '../../../test-utils';
 import { FileNode } from './FileNode';
 
+jest.mock('react-redux', () => ({
+  ...jest.requireActual('react-redux'),
+  useDispatch: jest.fn()
+}));
+
 describe('<FileNode />', () => {
+  const mockDispatch = jest.fn();
+  const mockStore = configureStore([]);
+
+  beforeEach(() => {
+    useDispatch.mockReturnValue(mockDispatch);
+  });
+
   const changeName = (newFileName) => {
     const renameButton = screen.getByText(/Rename/i);
     fireEvent.click(renameButton);
@@ -25,6 +40,19 @@ describe('<FileNode />', () => {
   };
 
   const renderFileNode = (fileType, extraProps = {}) => {
+    const initialState = {
+      files: [
+        {
+          id: '0',
+          name: fileType === 'folder' ? 'afolder' : 'test.jsx',
+          fileType
+        }
+      ],
+      user: { authenticated: false }
+    };
+
+    const store = mockStore(initialState);
+
     const props = {
       ...extraProps,
       id: '0',
@@ -45,24 +73,28 @@ describe('<FileNode />', () => {
       setProjectName: jest.fn()
     };
 
-    render(<FileNode {...props} />);
+    render(
+      <Provider store={store}>
+        <FileNode {...props} />
+      </Provider>
+    );
 
-    return props;
+    return { store, props };
   };
 
   describe('fileType: file', () => {
     it('cannot change to an empty name', async () => {
-      const props = renderFileNode('file');
+      const { props } = renderFileNode('file');
 
       changeName('');
 
-      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
       await expectFileNameToBe(props.name);
     });
 
     it('can change to a valid filename', async () => {
       const newName = 'newname.jsx';
-      const props = renderFileNode('file');
+      const { props } = renderFileNode('file');
 
       changeName(newName);
 
@@ -74,11 +106,11 @@ describe('<FileNode />', () => {
 
     it('must have an extension', async () => {
       const newName = 'newname';
-      const props = renderFileNode('file');
+      const { props } = renderFileNode('file');
 
       changeName(newName);
 
-      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
       await expectFileNameToBe(props.name);
     });
 
@@ -87,7 +119,7 @@ describe('<FileNode />', () => {
       window.confirm = mockConfirm;
 
       const newName = 'newname.gif';
-      const props = renderFileNode('file');
+      const { props } = renderFileNode('file');
 
       changeName(newName);
 
@@ -95,33 +127,33 @@ describe('<FileNode />', () => {
       await waitFor(() =>
         expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
       );
-      await expectFileNameToBe(props.name);
+      await expectFileNameToBe(newName);
     });
 
     it('cannot be just an extension', async () => {
       const newName = '.jsx';
-      const props = renderFileNode('file');
+      const { props } = renderFileNode('file');
 
       changeName(newName);
 
-      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
       await expectFileNameToBe(props.name);
     });
   });
 
   describe('fileType: folder', () => {
     it('cannot change to an empty name', async () => {
-      const props = renderFileNode('folder');
+      const { props } = renderFileNode('folder');
 
       changeName('');
 
-      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
       await expectFileNameToBe(props.name);
     });
 
     it('can change to another name', async () => {
       const newName = 'foldername';
-      const props = renderFileNode('folder');
+      const { props } = renderFileNode('folder');
 
       changeName(newName);
 
@@ -133,11 +165,11 @@ describe('<FileNode />', () => {
 
     it('cannot have a file extension', async () => {
       const newName = 'foldername.jsx';
-      const props = renderFileNode('folder');
+      const { props } = renderFileNode('folder');
 
       changeName(newName);
 
-      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
       await expectFileNameToBe(props.name);
     });
   });

From 5c8a91249baf29b96ff20833b277107e21e79db4 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Thu, 28 Nov 2024 10:06:18 -0500
Subject: [PATCH 15/39] update dispatch and add selectors

---
 client/modules/IDE/components/FileNode.jsx | 132 +++++----------------
 1 file changed, 31 insertions(+), 101 deletions(-)

diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx
index 04d7223bdb..cd6caa3255 100644
--- a/client/modules/IDE/components/FileNode.jsx
+++ b/client/modules/IDE/components/FileNode.jsx
@@ -1,43 +1,17 @@
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import React, { useState, useRef } from 'react';
-import { connect, useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
 import { useTranslation } from 'react-i18next';
 
 import * as IDEActions from '../actions/ide';
 import * as FileActions from '../actions/files';
+import parseFileName from '../utils/parseFileName';
 import DownArrowIcon from '../../../images/down-filled-triangle.svg';
 import FolderRightIcon from '../../../images/triangle-arrow-right.svg';
 import FolderDownIcon from '../../../images/triangle-arrow-down.svg';
 import FileTypeIcon from './FileTypeIcon';
 
-function parseFileName(name) {
-  const nameArray = name.split('.');
-  if (nameArray.length > 1) {
-    const extension = `.${nameArray[nameArray.length - 1]}`;
-    const baseName = nameArray.slice(0, -1).join('.');
-    const firstLetter = baseName[0];
-    const lastLetter = baseName[baseName.length - 1];
-    const middleText = baseName.slice(1, -1);
-    return {
-      baseName,
-      firstLetter,
-      lastLetter,
-      middleText,
-      extension
-    };
-  }
-  const firstLetter = name[0];
-  const lastLetter = name[name.length - 1];
-  const middleText = name.slice(1, -1);
-  return {
-    baseName: name,
-    firstLetter,
-    lastLetter,
-    middleText
-  };
-}
-
 function FileName({ name }) {
   const {
     baseName,
@@ -62,41 +36,35 @@ FileName.propTypes = {
   name: PropTypes.string.isRequired
 };
 
-const FileNode = ({
-  id,
-  parentId,
-  children,
-  name,
-  fileType,
-  isSelectedFile,
-  isFolderClosed,
-  setSelectedFile,
-  deleteFile,
-  updateFileName,
-  resetSelectedFile,
-  newFile,
-  newFolder,
-  showFolderChildren,
-  hideFolderChildren,
-  canEdit,
-  openUploadFileModal,
-  authenticated,
-  onClickFile
-}) => {
+const FileNode = ({ id, canEdit, onClickFile }) => {
+  const dispatch = useDispatch();
+  const { t } = useTranslation();
+
+  const fileNode =
+    useSelector((state) => state.files.find((file) => file.id === id)) || {};
+  const authenticated = useSelector((state) => state.user.authenticated);
+
+  const {
+    name = '',
+    parentId = null,
+    children = [],
+    fileType = 'file',
+    isSelectedFile = false,
+    isFolderClosed = false
+  } = fileNode;
+
   const [isOptionsOpen, setIsOptionsOpen] = useState(false);
   const [isEditingName, setIsEditingName] = useState(false);
   const [isDeleting, setIsDeleting] = useState(false);
   const [updatedName, setUpdatedName] = useState(name);
-  const dispatch = useDispatch();
 
-  const { t } = useTranslation();
   const fileNameInput = useRef(null);
   const fileOptionsRef = useRef(null);
 
   const handleFileClick = (event) => {
     event.stopPropagation();
     if (name !== 'root' && !isDeleting) {
-      setSelectedFile(id);
+      dispatch(IDEActions.setSelectedFile(id));
     }
     if (onClickFile) {
       onClickFile();
@@ -123,17 +91,17 @@ const FileNode = ({
   };
 
   const handleClickAddFile = () => {
-    dispatch(newFile(id));
+    dispatch(IDEActions.newFile(id));
     setTimeout(() => hideFileOptions(), 0);
   };
 
   const handleClickAddFolder = () => {
-    dispatch(newFolder(id));
+    dispatch(IDEActions.newFolder(id));
     setTimeout(() => hideFileOptions(), 0);
   };
 
   const handleClickUploadFile = () => {
-    dispatch(openUploadFileModal(id));
+    dispatch(IDEActions.openUploadFileModal(id));
     setTimeout(hideFileOptions, 0);
   };
 
@@ -142,8 +110,8 @@ const FileNode = ({
 
     if (window.confirm(prompt)) {
       setIsDeleting(true);
-      resetSelectedFile(id);
-      setTimeout(() => deleteFile(id, parentId), 100);
+      dispatch(IDEActions.resetSelectedFile(id));
+      setTimeout(() => dispatch(FileActions.deleteFile(id, parentId), 100));
     }
   };
 
@@ -159,7 +127,7 @@ const FileNode = ({
 
   const saveUpdatedFileName = () => {
     if (updatedName !== name) {
-      updateFileName(id, updatedName);
+      dispatch(FileActions.updateFileName(id, updatedName));
     }
   };
 
@@ -244,7 +212,7 @@ const FileNode = ({
             <div className="sidebar__file-item--folder">
               <button
                 className="sidebar__file-item-closed"
-                onClick={() => showFolderChildren(id)}
+                onClick={() => dispatch(FileActions.showFolderChildren(id))}
                 aria-label={t('FileNode.OpenFolderARIA')}
                 title={t('FileNode.OpenFolderARIA')}
               >
@@ -256,7 +224,7 @@ const FileNode = ({
               </button>
               <button
                 className="sidebar__file-item-open"
-                onClick={() => hideFolderChildren(id)}
+                onClick={() => dispatch(FileActions.hideFolderChildren(id))}
                 aria-label={t('FileNode.CloseFolderARIA')}
                 title={t('FileNode.CloseFolderARIA')}
               >
@@ -354,7 +322,7 @@ const FileNode = ({
         <ul className="file-item__children">
           {children.map((childId) => (
             <li key={childId}>
-              <ConnectedFileNode
+              <FileNode
                 id={childId}
                 parentId={id}
                 canEdit={canEdit}
@@ -370,50 +338,12 @@ const FileNode = ({
 
 FileNode.propTypes = {
   id: PropTypes.string.isRequired,
-  parentId: PropTypes.string,
-  children: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
-  name: PropTypes.string.isRequired,
-  fileType: PropTypes.string.isRequired,
-  isSelectedFile: PropTypes.bool,
-  isFolderClosed: PropTypes.bool,
-  setSelectedFile: PropTypes.func.isRequired,
-  deleteFile: PropTypes.func.isRequired,
-  updateFileName: PropTypes.func.isRequired,
-  resetSelectedFile: PropTypes.func.isRequired,
-  newFile: PropTypes.func.isRequired,
-  newFolder: PropTypes.func.isRequired,
-  showFolderChildren: PropTypes.func.isRequired,
-  hideFolderChildren: PropTypes.func.isRequired,
   canEdit: PropTypes.bool.isRequired,
-  openUploadFileModal: PropTypes.func.isRequired,
-  authenticated: PropTypes.bool.isRequired,
   onClickFile: PropTypes.func
 };
 
 FileNode.defaultProps = {
-  onClickFile: null,
-  parentId: '0',
-  isSelectedFile: false,
-  isFolderClosed: false
+  onClickFile: null
 };
 
-function mapStateToProps(state, ownProps) {
-  // this is a hack, state is updated before ownProps
-  const fileNode = state.files.find((file) => file.id === ownProps.id) || {
-    name: 'test',
-    fileType: 'file'
-  };
-  return Object.assign({}, fileNode, {
-    authenticated: state.user.authenticated
-  });
-}
-
-const mapDispatchToProps = { ...FileActions, ...IDEActions };
-
-const ConnectedFileNode = connect(
-  mapStateToProps,
-  mapDispatchToProps
-)(FileNode);
-
-export { FileNode };
-export default ConnectedFileNode;
+export default FileNode;

From 2177976d9f480185c3ac44cd2f3278252caf83fa Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Thu, 28 Nov 2024 10:06:42 -0500
Subject: [PATCH 16/39] create parseFileName util

---
 client/modules/IDE/utils/parseFileName.js | 28 +++++++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 client/modules/IDE/utils/parseFileName.js

diff --git a/client/modules/IDE/utils/parseFileName.js b/client/modules/IDE/utils/parseFileName.js
new file mode 100644
index 0000000000..1bde7be0f6
--- /dev/null
+++ b/client/modules/IDE/utils/parseFileName.js
@@ -0,0 +1,28 @@
+function parseFileName(name) {
+  const nameArray = name.split('.');
+  if (nameArray.length > 1) {
+    const extension = `.${nameArray[nameArray.length - 1]}`;
+    const baseName = nameArray.slice(0, -1).join('.');
+    const firstLetter = baseName[0];
+    const lastLetter = baseName[baseName.length - 1];
+    const middleText = baseName.slice(1, -1);
+    return {
+      baseName,
+      firstLetter,
+      lastLetter,
+      middleText,
+      extension
+    };
+  }
+  const firstLetter = name[0];
+  const lastLetter = name[name.length - 1];
+  const middleText = name.slice(1, -1);
+  return {
+    baseName: name,
+    firstLetter,
+    lastLetter,
+    middleText
+  };
+}
+
+export default parseFileName;

From 0e9a96712716fca8c5d29510bd7cfc2d80a752eb Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Thu, 28 Nov 2024 10:07:04 -0500
Subject: [PATCH 17/39] update FileNode tests

---
 .../IDE/components/FileNode.unit.test.jsx     | 78 +++++++++----------
 1 file changed, 35 insertions(+), 43 deletions(-)

diff --git a/client/modules/IDE/components/FileNode.unit.test.jsx b/client/modules/IDE/components/FileNode.unit.test.jsx
index 86d3504626..4f75d9a272 100644
--- a/client/modules/IDE/components/FileNode.unit.test.jsx
+++ b/client/modules/IDE/components/FileNode.unit.test.jsx
@@ -2,6 +2,7 @@ import React from 'react';
 import { Provider } from 'react-redux';
 import configureStore from 'redux-mock-store';
 import { useDispatch } from 'react-redux';
+import * as FileActions from '../actions/files';
 
 import {
   fireEvent,
@@ -10,19 +11,25 @@ import {
   waitFor,
   within
 } from '../../../test-utils';
-import { FileNode } from './FileNode';
+import FileNode from './FileNode';
 
 jest.mock('react-redux', () => ({
   ...jest.requireActual('react-redux'),
   useDispatch: jest.fn()
 }));
 
+jest.mock('../actions/files', () => ({
+  updateFileName: jest.fn()
+}));
+
+const mockStore = configureStore([]);
+
 describe('<FileNode />', () => {
   const mockDispatch = jest.fn();
-  const mockStore = configureStore([]);
 
   beforeEach(() => {
     useDispatch.mockReturnValue(mockDispatch);
+    jest.clearAllMocks();
   });
 
   const changeName = (newFileName) => {
@@ -39,79 +46,64 @@ describe('<FileNode />', () => {
     await waitFor(() => within(name).queryByText(expectedName));
   };
 
-  const renderFileNode = (fileType, extraProps = {}) => {
+  const renderFileNode = (fileType, extraState = {}) => {
     const initialState = {
       files: [
         {
           id: '0',
           name: fileType === 'folder' ? 'afolder' : 'test.jsx',
-          fileType
+          fileType,
+          parentId: 'root',
+          children: [],
+          isSelectedFile: false,
+          isFolderClosed: false
         }
       ],
-      user: { authenticated: false }
+      user: { authenticated: false },
+      ...extraState
     };
 
     const store = mockStore(initialState);
 
-    const props = {
-      ...extraProps,
-      id: '0',
-      name: fileType === 'folder' ? 'afolder' : 'test.jsx',
-      fileType,
-      canEdit: true,
-      children: [],
-      authenticated: false,
-      setSelectedFile: jest.fn(),
-      deleteFile: jest.fn(),
-      updateFileName: jest.fn(),
-      resetSelectedFile: jest.fn(),
-      newFile: jest.fn(),
-      newFolder: jest.fn(),
-      showFolderChildren: jest.fn(),
-      hideFolderChildren: jest.fn(),
-      openUploadFileModal: jest.fn(),
-      setProjectName: jest.fn()
-    };
-
     render(
       <Provider store={store}>
-        <FileNode {...props} />
+        <FileNode id="0" canEdit />
       </Provider>
     );
 
-    return { store, props };
+    return { store };
   };
 
   describe('fileType: file', () => {
     it('cannot change to an empty name', async () => {
-      const { props } = renderFileNode('file');
+      renderFileNode('file');
 
       changeName('');
 
       await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe(props.name);
+      await expectFileNameToBe('test.jsx');
     });
 
     it('can change to a valid filename', async () => {
       const newName = 'newname.jsx';
-      const { props } = renderFileNode('file');
+      renderFileNode('file');
 
       changeName(newName);
 
       await waitFor(() =>
-        expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
+        expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
       );
       await expectFileNameToBe(newName);
     });
 
     it('must have an extension', async () => {
       const newName = 'newname';
-      const { props } = renderFileNode('file');
+      renderFileNode('file');
 
       changeName(newName);
 
       await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe(props.name);
+      await expectFileNameToBe('test.jsx');
     });
 
     it('can change to a different extension', async () => {
@@ -119,58 +111,58 @@ describe('<FileNode />', () => {
       window.confirm = mockConfirm;
 
       const newName = 'newname.gif';
-      const { props } = renderFileNode('file');
+      renderFileNode('file');
 
       changeName(newName);
 
       expect(mockConfirm).toHaveBeenCalled();
       await waitFor(() =>
-        expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
+        expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
       );
       await expectFileNameToBe(newName);
     });
 
     it('cannot be just an extension', async () => {
       const newName = '.jsx';
-      const { props } = renderFileNode('file');
+      renderFileNode('file');
 
       changeName(newName);
 
       await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe(props.name);
+      await expectFileNameToBe('test.jsx');
     });
   });
 
   describe('fileType: folder', () => {
     it('cannot change to an empty name', async () => {
-      const { props } = renderFileNode('folder');
+      renderFileNode('folder');
 
       changeName('');
 
       await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe(props.name);
+      await expectFileNameToBe('afolder');
     });
 
     it('can change to another name', async () => {
       const newName = 'foldername';
-      const { props } = renderFileNode('folder');
+      renderFileNode('folder');
 
       changeName(newName);
 
       await waitFor(() =>
-        expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
+        expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
       );
       await expectFileNameToBe(newName);
     });
 
     it('cannot have a file extension', async () => {
       const newName = 'foldername.jsx';
-      const { props } = renderFileNode('folder');
+      renderFileNode('folder');
 
       changeName(newName);
 
       await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe(props.name);
+      await expectFileNameToBe('afolder');
     });
   });
 });

From fd3e3a6e98da361250c596a19b6bafd7588ec771 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Thu, 28 Nov 2024 10:07:30 -0500
Subject: [PATCH 18/39] update FileNode import

---
 client/modules/IDE/components/Sidebar.jsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx
index 79ffed3b86..20f0104caf 100644
--- a/client/modules/IDE/components/Sidebar.jsx
+++ b/client/modules/IDE/components/Sidebar.jsx
@@ -13,7 +13,7 @@ import {
 import { selectRootFile } from '../selectors/files';
 import { getAuthenticated, selectCanEditSketch } from '../selectors/users';
 
-import ConnectedFileNode from './FileNode';
+import FileNode from './FileNode';
 import { PlusIcon } from '../../../common/icons';
 import { FileDrawer } from './Editor/MobileEditor';
 
@@ -130,7 +130,7 @@ export default function SideBar() {
             </ul>
           </div>
         </header>
-        <ConnectedFileNode id={rootFile.id} canEdit={canEditProject} />
+        <FileNode id={rootFile.id} canEdit={canEditProject} />
       </section>
     </FileDrawer>
   );

From 48f9ad8ab90e9abbcc2bc35b563a0289a7b8b39e Mon Sep 17 00:00:00 2001
From: Stef Tervelde <stef@steftervelde.nl>
Date: Sat, 30 Nov 2024 20:01:07 +0100
Subject: [PATCH 19/39] added donation banner

---
 client/index.jsx              | 14 ++++++++++++++
 client/styles/base/_base.scss |  7 +++++++
 2 files changed, 21 insertions(+)

diff --git a/client/index.jsx b/client/index.jsx
index 6c12fa511b..313334dc20 100644
--- a/client/index.jsx
+++ b/client/index.jsx
@@ -20,6 +20,20 @@ const initialState = window.__INITIAL_STATE__;
 
 const store = configureStore(initialState);
 
+// Add a banner to the page
+const banner = document.createElement('div');
+banner.id = 'processing-banner';
+document.body.appendChild(banner);
+
+const link = document.createElement('link');
+link.rel = 'stylesheet';
+link.href = 'https://foundation-donate-banner.netlify.app/static/css/main.css';
+document.head.appendChild(link);
+
+const script = document.createElement('script');
+script.src = 'https://foundation-donate-banner.netlify.app/static/js/main.js';
+document.body.appendChild(script);
+
 const App = () => (
   <>
     <Router history={browserHistory}>
diff --git a/client/styles/base/_base.scss b/client/styles/base/_base.scss
index 3e283d7ab9..ad17249932 100644
--- a/client/styles/base/_base.scss
+++ b/client/styles/base/_base.scss
@@ -151,3 +151,10 @@ textarea:focus {
 	white-space: nowrap;
 	width: 1px;
 }
+
+
+// Donate banner custom properties
+body {
+	--donate-banner-dark: #c01c4c;
+	--donate-banner-background: url('https://foundation-donate-banner.netlify.app/p5.png');
+}
\ No newline at end of file

From f84ebd9e05a01e1b4283783e7a6bfb37916f6a2c Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Mon, 2 Dec 2024 20:45:14 -0500
Subject: [PATCH 20/39] 2.15.4

---
 package-lock.json | 4 ++--
 package.json      | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index de6eb98b9d..0d1b8cecf9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "p5.js-web-editor",
-  "version": "2.15.3",
+  "version": "2.15.4",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "p5.js-web-editor",
-      "version": "2.15.3",
+      "version": "2.15.4",
       "license": "LGPL-2.1",
       "dependencies": {
         "@auth0/s3": "^1.0.0",
diff --git a/package.json b/package.json
index 40c364e6a8..03c93a5be7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "p5.js-web-editor",
-  "version": "2.15.3",
+  "version": "2.15.4",
   "description": "The web editor for p5.js.",
   "scripts": {
     "clean": "rimraf dist",

From a361329db1856eea016c82c3ce1a627e696849e4 Mon Sep 17 00:00:00 2001
From: raclim <43053081+raclim@users.noreply.github.com>
Date: Tue, 3 Dec 2024 00:42:08 -0500
Subject: [PATCH 21/39] Revert "Fix Console Errors and Update Hooks in
 FileNode"

---
 .../modules/IDE/components/ConsoleInput.jsx   |   4 +-
 client/modules/IDE/components/FileNode.jsx    | 131 +++++++++++++-----
 .../IDE/components/FileNode.unit.test.jsx     | 112 ++++++---------
 client/modules/IDE/components/Sidebar.jsx     |   4 +-
 client/modules/IDE/utils/parseFileName.js     |  28 ----
 5 files changed, 147 insertions(+), 132 deletions(-)
 delete mode 100644 client/modules/IDE/utils/parseFileName.js

diff --git a/client/modules/IDE/components/ConsoleInput.jsx b/client/modules/IDE/components/ConsoleInput.jsx
index daac76839c..c026d29d4b 100644
--- a/client/modules/IDE/components/ConsoleInput.jsx
+++ b/client/modules/IDE/components/ConsoleInput.jsx
@@ -1,6 +1,5 @@
 import PropTypes from 'prop-types';
 import React, { useRef, useEffect, useState } from 'react';
-import { useDispatch } from 'react-redux';
 import CodeMirror from 'codemirror';
 import { Encode } from 'console-feed';
 
@@ -16,7 +15,6 @@ function ConsoleInput({ theme, fontSize }) {
   const [commandCursor, setCommandCursor] = useState(-1);
   const codemirrorContainer = useRef(null);
   const cmInstance = useRef(null);
-  const dispatch = useDispatch();
 
   useEffect(() => {
     cmInstance.current = CodeMirror(codemirrorContainer.current, {
@@ -47,7 +45,7 @@ function ConsoleInput({ theme, fontSize }) {
           payload: { source: 'console', messages }
         });
 
-        dispatch(dispatchConsoleEvent(consoleEvent));
+        dispatchConsoleEvent(consoleEvent);
         cm.setValue('');
         setCommandHistory([value, ...commandHistory]);
         setCommandCursor(-1);
diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx
index cd6caa3255..e589fb3cff 100644
--- a/client/modules/IDE/components/FileNode.jsx
+++ b/client/modules/IDE/components/FileNode.jsx
@@ -1,17 +1,43 @@
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import React, { useState, useRef } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { connect } from 'react-redux';
 import { useTranslation } from 'react-i18next';
 
 import * as IDEActions from '../actions/ide';
 import * as FileActions from '../actions/files';
-import parseFileName from '../utils/parseFileName';
 import DownArrowIcon from '../../../images/down-filled-triangle.svg';
 import FolderRightIcon from '../../../images/triangle-arrow-right.svg';
 import FolderDownIcon from '../../../images/triangle-arrow-down.svg';
 import FileTypeIcon from './FileTypeIcon';
 
+function parseFileName(name) {
+  const nameArray = name.split('.');
+  if (nameArray.length > 1) {
+    const extension = `.${nameArray[nameArray.length - 1]}`;
+    const baseName = nameArray.slice(0, -1).join('.');
+    const firstLetter = baseName[0];
+    const lastLetter = baseName[baseName.length - 1];
+    const middleText = baseName.slice(1, -1);
+    return {
+      baseName,
+      firstLetter,
+      lastLetter,
+      middleText,
+      extension
+    };
+  }
+  const firstLetter = name[0];
+  const lastLetter = name[name.length - 1];
+  const middleText = name.slice(1, -1);
+  return {
+    baseName: name,
+    firstLetter,
+    lastLetter,
+    middleText
+  };
+}
+
 function FileName({ name }) {
   const {
     baseName,
@@ -36,35 +62,40 @@ FileName.propTypes = {
   name: PropTypes.string.isRequired
 };
 
-const FileNode = ({ id, canEdit, onClickFile }) => {
-  const dispatch = useDispatch();
-  const { t } = useTranslation();
-
-  const fileNode =
-    useSelector((state) => state.files.find((file) => file.id === id)) || {};
-  const authenticated = useSelector((state) => state.user.authenticated);
-
-  const {
-    name = '',
-    parentId = null,
-    children = [],
-    fileType = 'file',
-    isSelectedFile = false,
-    isFolderClosed = false
-  } = fileNode;
-
+const FileNode = ({
+  id,
+  parentId,
+  children,
+  name,
+  fileType,
+  isSelectedFile,
+  isFolderClosed,
+  setSelectedFile,
+  deleteFile,
+  updateFileName,
+  resetSelectedFile,
+  newFile,
+  newFolder,
+  showFolderChildren,
+  hideFolderChildren,
+  canEdit,
+  openUploadFileModal,
+  authenticated,
+  onClickFile
+}) => {
   const [isOptionsOpen, setIsOptionsOpen] = useState(false);
   const [isEditingName, setIsEditingName] = useState(false);
   const [isDeleting, setIsDeleting] = useState(false);
   const [updatedName, setUpdatedName] = useState(name);
 
+  const { t } = useTranslation();
   const fileNameInput = useRef(null);
   const fileOptionsRef = useRef(null);
 
   const handleFileClick = (event) => {
     event.stopPropagation();
     if (name !== 'root' && !isDeleting) {
-      dispatch(IDEActions.setSelectedFile(id));
+      setSelectedFile(id);
     }
     if (onClickFile) {
       onClickFile();
@@ -91,17 +122,17 @@ const FileNode = ({ id, canEdit, onClickFile }) => {
   };
 
   const handleClickAddFile = () => {
-    dispatch(IDEActions.newFile(id));
+    newFile(id);
     setTimeout(() => hideFileOptions(), 0);
   };
 
   const handleClickAddFolder = () => {
-    dispatch(IDEActions.newFolder(id));
+    newFolder(id);
     setTimeout(() => hideFileOptions(), 0);
   };
 
   const handleClickUploadFile = () => {
-    dispatch(IDEActions.openUploadFileModal(id));
+    openUploadFileModal(id);
     setTimeout(hideFileOptions, 0);
   };
 
@@ -110,8 +141,8 @@ const FileNode = ({ id, canEdit, onClickFile }) => {
 
     if (window.confirm(prompt)) {
       setIsDeleting(true);
-      dispatch(IDEActions.resetSelectedFile(id));
-      setTimeout(() => dispatch(FileActions.deleteFile(id, parentId), 100));
+      resetSelectedFile(id);
+      setTimeout(() => deleteFile(id, parentId), 100);
     }
   };
 
@@ -127,7 +158,7 @@ const FileNode = ({ id, canEdit, onClickFile }) => {
 
   const saveUpdatedFileName = () => {
     if (updatedName !== name) {
-      dispatch(FileActions.updateFileName(id, updatedName));
+      updateFileName(id, updatedName);
     }
   };
 
@@ -212,7 +243,7 @@ const FileNode = ({ id, canEdit, onClickFile }) => {
             <div className="sidebar__file-item--folder">
               <button
                 className="sidebar__file-item-closed"
-                onClick={() => dispatch(FileActions.showFolderChildren(id))}
+                onClick={() => showFolderChildren(id)}
                 aria-label={t('FileNode.OpenFolderARIA')}
                 title={t('FileNode.OpenFolderARIA')}
               >
@@ -224,7 +255,7 @@ const FileNode = ({ id, canEdit, onClickFile }) => {
               </button>
               <button
                 className="sidebar__file-item-open"
-                onClick={() => dispatch(FileActions.hideFolderChildren(id))}
+                onClick={() => hideFolderChildren(id)}
                 aria-label={t('FileNode.CloseFolderARIA')}
                 title={t('FileNode.CloseFolderARIA')}
               >
@@ -322,7 +353,7 @@ const FileNode = ({ id, canEdit, onClickFile }) => {
         <ul className="file-item__children">
           {children.map((childId) => (
             <li key={childId}>
-              <FileNode
+              <ConnectedFileNode
                 id={childId}
                 parentId={id}
                 canEdit={canEdit}
@@ -338,12 +369,50 @@ const FileNode = ({ id, canEdit, onClickFile }) => {
 
 FileNode.propTypes = {
   id: PropTypes.string.isRequired,
+  parentId: PropTypes.string,
+  children: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
+  name: PropTypes.string.isRequired,
+  fileType: PropTypes.string.isRequired,
+  isSelectedFile: PropTypes.bool,
+  isFolderClosed: PropTypes.bool,
+  setSelectedFile: PropTypes.func.isRequired,
+  deleteFile: PropTypes.func.isRequired,
+  updateFileName: PropTypes.func.isRequired,
+  resetSelectedFile: PropTypes.func.isRequired,
+  newFile: PropTypes.func.isRequired,
+  newFolder: PropTypes.func.isRequired,
+  showFolderChildren: PropTypes.func.isRequired,
+  hideFolderChildren: PropTypes.func.isRequired,
   canEdit: PropTypes.bool.isRequired,
+  openUploadFileModal: PropTypes.func.isRequired,
+  authenticated: PropTypes.bool.isRequired,
   onClickFile: PropTypes.func
 };
 
 FileNode.defaultProps = {
-  onClickFile: null
+  onClickFile: null,
+  parentId: '0',
+  isSelectedFile: false,
+  isFolderClosed: false
 };
 
-export default FileNode;
+function mapStateToProps(state, ownProps) {
+  // this is a hack, state is updated before ownProps
+  const fileNode = state.files.find((file) => file.id === ownProps.id) || {
+    name: 'test',
+    fileType: 'file'
+  };
+  return Object.assign({}, fileNode, {
+    authenticated: state.user.authenticated
+  });
+}
+
+const mapDispatchToProps = { ...FileActions, ...IDEActions };
+
+const ConnectedFileNode = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(FileNode);
+
+export { FileNode };
+export default ConnectedFileNode;
diff --git a/client/modules/IDE/components/FileNode.unit.test.jsx b/client/modules/IDE/components/FileNode.unit.test.jsx
index 4f75d9a272..8676a817d8 100644
--- a/client/modules/IDE/components/FileNode.unit.test.jsx
+++ b/client/modules/IDE/components/FileNode.unit.test.jsx
@@ -1,8 +1,4 @@
 import React from 'react';
-import { Provider } from 'react-redux';
-import configureStore from 'redux-mock-store';
-import { useDispatch } from 'react-redux';
-import * as FileActions from '../actions/files';
 
 import {
   fireEvent,
@@ -11,27 +7,9 @@ import {
   waitFor,
   within
 } from '../../../test-utils';
-import FileNode from './FileNode';
-
-jest.mock('react-redux', () => ({
-  ...jest.requireActual('react-redux'),
-  useDispatch: jest.fn()
-}));
-
-jest.mock('../actions/files', () => ({
-  updateFileName: jest.fn()
-}));
-
-const mockStore = configureStore([]);
+import { FileNode } from './FileNode';
 
 describe('<FileNode />', () => {
-  const mockDispatch = jest.fn();
-
-  beforeEach(() => {
-    useDispatch.mockReturnValue(mockDispatch);
-    jest.clearAllMocks();
-  });
-
   const changeName = (newFileName) => {
     const renameButton = screen.getByText(/Rename/i);
     fireEvent.click(renameButton);
@@ -46,64 +24,62 @@ describe('<FileNode />', () => {
     await waitFor(() => within(name).queryByText(expectedName));
   };
 
-  const renderFileNode = (fileType, extraState = {}) => {
-    const initialState = {
-      files: [
-        {
-          id: '0',
-          name: fileType === 'folder' ? 'afolder' : 'test.jsx',
-          fileType,
-          parentId: 'root',
-          children: [],
-          isSelectedFile: false,
-          isFolderClosed: false
-        }
-      ],
-      user: { authenticated: false },
-      ...extraState
+  const renderFileNode = (fileType, extraProps = {}) => {
+    const props = {
+      ...extraProps,
+      id: '0',
+      name: fileType === 'folder' ? 'afolder' : 'test.jsx',
+      fileType,
+      canEdit: true,
+      children: [],
+      authenticated: false,
+      setSelectedFile: jest.fn(),
+      deleteFile: jest.fn(),
+      updateFileName: jest.fn(),
+      resetSelectedFile: jest.fn(),
+      newFile: jest.fn(),
+      newFolder: jest.fn(),
+      showFolderChildren: jest.fn(),
+      hideFolderChildren: jest.fn(),
+      openUploadFileModal: jest.fn(),
+      setProjectName: jest.fn()
     };
 
-    const store = mockStore(initialState);
+    render(<FileNode {...props} />);
 
-    render(
-      <Provider store={store}>
-        <FileNode id="0" canEdit />
-      </Provider>
-    );
-
-    return { store };
+    return props;
   };
 
   describe('fileType: file', () => {
     it('cannot change to an empty name', async () => {
-      renderFileNode('file');
+      const props = renderFileNode('file');
 
       changeName('');
 
-      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe('test.jsx');
+      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await expectFileNameToBe(props.name);
     });
 
     it('can change to a valid filename', async () => {
       const newName = 'newname.jsx';
-      renderFileNode('file');
+      const props = renderFileNode('file');
 
       changeName(newName);
 
       await waitFor(() =>
-        expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
+        expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
       );
       await expectFileNameToBe(newName);
     });
 
     it('must have an extension', async () => {
       const newName = 'newname';
-      renderFileNode('file');
+      const props = renderFileNode('file');
 
       changeName(newName);
 
-      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe('test.jsx');
+      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await expectFileNameToBe(props.name);
     });
 
     it('can change to a different extension', async () => {
@@ -111,58 +87,58 @@ describe('<FileNode />', () => {
       window.confirm = mockConfirm;
 
       const newName = 'newname.gif';
-      renderFileNode('file');
+      const props = renderFileNode('file');
 
       changeName(newName);
 
       expect(mockConfirm).toHaveBeenCalled();
       await waitFor(() =>
-        expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
+        expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
       );
-      await expectFileNameToBe(newName);
+      await expectFileNameToBe(props.name);
     });
 
     it('cannot be just an extension', async () => {
       const newName = '.jsx';
-      renderFileNode('file');
+      const props = renderFileNode('file');
 
       changeName(newName);
 
-      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe('test.jsx');
+      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await expectFileNameToBe(props.name);
     });
   });
 
   describe('fileType: folder', () => {
     it('cannot change to an empty name', async () => {
-      renderFileNode('folder');
+      const props = renderFileNode('folder');
 
       changeName('');
 
-      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe('afolder');
+      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await expectFileNameToBe(props.name);
     });
 
     it('can change to another name', async () => {
       const newName = 'foldername';
-      renderFileNode('folder');
+      const props = renderFileNode('folder');
 
       changeName(newName);
 
       await waitFor(() =>
-        expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
+        expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
       );
       await expectFileNameToBe(newName);
     });
 
     it('cannot have a file extension', async () => {
       const newName = 'foldername.jsx';
-      renderFileNode('folder');
+      const props = renderFileNode('folder');
 
       changeName(newName);
 
-      await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
-      await expectFileNameToBe('afolder');
+      await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
+      await expectFileNameToBe(props.name);
     });
   });
 });
diff --git a/client/modules/IDE/components/Sidebar.jsx b/client/modules/IDE/components/Sidebar.jsx
index 20f0104caf..79ffed3b86 100644
--- a/client/modules/IDE/components/Sidebar.jsx
+++ b/client/modules/IDE/components/Sidebar.jsx
@@ -13,7 +13,7 @@ import {
 import { selectRootFile } from '../selectors/files';
 import { getAuthenticated, selectCanEditSketch } from '../selectors/users';
 
-import FileNode from './FileNode';
+import ConnectedFileNode from './FileNode';
 import { PlusIcon } from '../../../common/icons';
 import { FileDrawer } from './Editor/MobileEditor';
 
@@ -130,7 +130,7 @@ export default function SideBar() {
             </ul>
           </div>
         </header>
-        <FileNode id={rootFile.id} canEdit={canEditProject} />
+        <ConnectedFileNode id={rootFile.id} canEdit={canEditProject} />
       </section>
     </FileDrawer>
   );
diff --git a/client/modules/IDE/utils/parseFileName.js b/client/modules/IDE/utils/parseFileName.js
deleted file mode 100644
index 1bde7be0f6..0000000000
--- a/client/modules/IDE/utils/parseFileName.js
+++ /dev/null
@@ -1,28 +0,0 @@
-function parseFileName(name) {
-  const nameArray = name.split('.');
-  if (nameArray.length > 1) {
-    const extension = `.${nameArray[nameArray.length - 1]}`;
-    const baseName = nameArray.slice(0, -1).join('.');
-    const firstLetter = baseName[0];
-    const lastLetter = baseName[baseName.length - 1];
-    const middleText = baseName.slice(1, -1);
-    return {
-      baseName,
-      firstLetter,
-      lastLetter,
-      middleText,
-      extension
-    };
-  }
-  const firstLetter = name[0];
-  const lastLetter = name[name.length - 1];
-  const middleText = name.slice(1, -1);
-  return {
-    baseName: name,
-    firstLetter,
-    lastLetter,
-    middleText
-  };
-}
-
-export default parseFileName;

From b63c031c6f42498b2512a1a12a06eacb88eff5a9 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Tue, 3 Dec 2024 00:51:56 -0500
Subject: [PATCH 22/39] add back in consoleinput changes

---
 client/modules/IDE/components/ConsoleInput.jsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/client/modules/IDE/components/ConsoleInput.jsx b/client/modules/IDE/components/ConsoleInput.jsx
index c026d29d4b..62e7da13a9 100644
--- a/client/modules/IDE/components/ConsoleInput.jsx
+++ b/client/modules/IDE/components/ConsoleInput.jsx
@@ -1,6 +1,7 @@
 import PropTypes from 'prop-types';
 import React, { useRef, useEffect, useState } from 'react';
 import CodeMirror from 'codemirror';
+import { useDispatch } from 'react-redux';
 import { Encode } from 'console-feed';
 
 import RightArrowIcon from '../../../images/right-arrow.svg';
@@ -15,6 +16,7 @@ function ConsoleInput({ theme, fontSize }) {
   const [commandCursor, setCommandCursor] = useState(-1);
   const codemirrorContainer = useRef(null);
   const cmInstance = useRef(null);
+  const dispatch = useDispatch();
 
   useEffect(() => {
     cmInstance.current = CodeMirror(codemirrorContainer.current, {
@@ -45,7 +47,7 @@ function ConsoleInput({ theme, fontSize }) {
           payload: { source: 'console', messages }
         });
 
-        dispatchConsoleEvent(consoleEvent);
+        dispatch(dispatchConsoleEvent(consoleEvent));
         cm.setValue('');
         setCommandHistory([value, ...commandHistory]);
         setCommandCursor(-1);

From c8118f639550c738d8df72542539259d939b2118 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Tue, 3 Dec 2024 00:55:50 -0500
Subject: [PATCH 23/39] 2.15.5

---
 package-lock.json | 4 ++--
 package.json      | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 0d1b8cecf9..3235c91550 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "p5.js-web-editor",
-  "version": "2.15.4",
+  "version": "2.15.5",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "p5.js-web-editor",
-      "version": "2.15.4",
+      "version": "2.15.5",
       "license": "LGPL-2.1",
       "dependencies": {
         "@auth0/s3": "^1.0.0",
diff --git a/package.json b/package.json
index 03c93a5be7..8d26a46a4a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "p5.js-web-editor",
-  "version": "2.15.4",
+  "version": "2.15.5",
   "description": "The web editor for p5.js.",
   "scripts": {
     "clean": "rimraf dist",

From 16644f3de1904bcf8418e99d6a6027e661afc87a Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 4 Dec 2024 10:42:18 -0500
Subject: [PATCH 24/39] add apple pay id

---
 static/.well-known/apple-developer-merchantid-domain-association | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 static/.well-known/apple-developer-merchantid-domain-association

diff --git a/static/.well-known/apple-developer-merchantid-domain-association b/static/.well-known/apple-developer-merchantid-domain-association
new file mode 100644
index 0000000000..2ff95c9628
--- /dev/null
+++ b/static/.well-known/apple-developer-merchantid-domain-association
@@ -0,0 +1 @@
+7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313536363233343735303036312C227369676E6174757265223A22333038303036303932613836343838366637306430313037303261303830333038303032303130313331306633303064303630393630383634383031363530333034303230313035303033303830303630393261383634383836663730643031303730313030303061303830333038323033653333303832303338386130303330323031303230323038346333303431343935313964353433363330306130363038326138363438636533643034303330323330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313339333033353331333833303331333333323335333735613137306433323334333033353331333633303331333333323335333735613330356633313235333032333036303335353034303330633163363536333633326437333664373032643632373236663662363537323264373336393637366535663535343333343264353035323466343433313134333031323036303335353034306230633062363934663533323035333739373337343635366437333331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034633231353737656465626436633762323231386636386464373039306131323138646337623062643666326332383364383436303935643934616634613534313162383334323065643831316633343037653833333331663163353463336637656233323230643662616435643465666634393238393839336537633066313361333832303231313330383230323064333030633036303335353164313330313031666630343032333030303330316630363033353531643233303431383330313638303134323366323439633434663933653465663237653663346636323836633366613262626664326534623330343530363038326230363031303530353037303130313034333933303337333033353036303832623036303130353035303733303031383632393638373437343730336132663266366636333733373032653631373037303663363532653633366636643266366636333733373033303334326436313730373036633635363136393633363133333330333233303832303131643036303335353164323030343832303131343330383230313130333038323031306330363039326138363438383666373633363430353031333038316665333038316333303630383262303630313035303530373032303233303831623630633831623335323635366336393631366536333635323036663665323037343638363937333230363336353732373436393636363936333631373436353230363237393230363136653739323037303631373237343739323036313733373337353664363537333230363136333633363537303734363136653633363532303666363632303734363836353230373436383635366532303631373037303663363936333631363236633635323037333734363136653634363137323634323037343635373236643733323036313665363432303633366636653634363937343639366636653733323036663636323037353733363532633230363336353732373436393636363936333631373436353230373036663663363936333739323036313665363432303633363537323734363936363639363336313734363936663665323037303732363136333734363936333635323037333734363137343635366436353665373437333265333033363036303832623036303130353035303730323031313632613638373437343730336132663266373737373737326536313730373036633635326536333666366432663633363537323734363936363639363336313734363536313735373436383666373236393734373932663330333430363033353531643166303432643330326233303239613032376130323538363233363837343734373033613266326636333732366332653631373037303663363532653633366636643266363137303730366336353631363936333631333332653633373236633330316430363033353531643065303431363034313439343537646236666435373438313836383938393736326637653537383530376537396235383234333030653036303335353164306630313031666630343034303330323037383033303066303630393261383634383836663736333634303631643034303230353030333030613036303832613836343863653364303430333032303334393030333034363032323130306265303935373166653731653165373335623535653561666163623463373266656234343566333031383532323263373235313030326236316562643666353530323231303064313862333530613564643664643665623137343630333562313165623263653837636661336536616636636264383338303839306463383263646461613633333038323032656533303832303237356130303330323031303230323038343936643266626633613938646139373330306130363038326138363438636533643034303330323330363733313162333031393036303335353034303330633132343137303730366336353230353236663666373432303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330316531373064333133343330333533303336333233333334333633333330356131373064333233393330333533303336333233333334333633333330356133303761333132653330326330363033353530343033306332353431373037303663363532303431373037303663363936333631373436393666366532303439366537343635363737323631373436393666366532303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330353933303133303630373261383634386365336430323031303630383261383634386365336430333031303730333432303030346630313731313834313964373634383564353161356532353831303737366538383061326566646537626165346465303864666334623933653133333536643536363562333561653232643039373736306432323465376262613038666437363137636538386362373662623636373062656338653832393834666635343435613338316637333038316634333034363036303832623036303130353035303730313031303433613330333833303336303630383262303630313035303530373330303138363261363837343734373033613266326636663633373337303265363137303730366336353265363336663664326636663633373337303330333432643631373037303663363537323666366637343633363136373333333031643036303335353164306530343136303431343233663234396334346639336534656632376536633466363238366333666132626266643265346233303066303630333535316431333031303166663034303533303033303130316666333031663036303335353164323330343138333031363830313462626230646561313538333338383961613438613939646562656264656261666461636232346162333033373036303335353164316630343330333032653330326361303261613032383836323636383734373437303361326632663633373236633265363137303730366336353265363336663664326636313730373036633635373236663666373436333631363733333265363337323663333030653036303335353164306630313031666630343034303330323031303633303130303630613261383634383836663736333634303630323065303430323035303033303061303630383261383634386365336430343033303230333637303033303634303233303361636637323833353131363939623138366662333563333536636136326266663431376564643930663735346461323865626566313963383135653432623738396638393866373962353939663938643534313064386639646539633266653032333033323264643534343231623061333035373736633564663333383362393036376664313737633263323136643936346663363732363938323132366635346638376137643162393963623962303938393231363130363939306630393932316430303030333138323031386233303832303138373032303130313330383138363330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533303230383463333034313439353139643534333633303064303630393630383634383031363530333034303230313035303061303831393533303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333933303338333133393331333733313332333333303561333032613036303932613836343838366637306430313039333433313164333031623330306430363039363038363438303136353033303430323031303530306131306130363038326138363438636533643034303330323330326630363039326138363438383666373064303130393034333132323034323062303731303365313430613462386231376262613230316130336163643036396234653431366232613263383066383661383338313435633239373566633131333030613036303832613836343863653364303430333032303434363330343430323230343639306264636637626461663833636466343934396534633035313039656463663334373665303564373261313264376335666538633033303033343464663032323032363764353863393365626233353031333836363062353730373938613064643731313734316262353864626436613138363633353038353431656565393035303030303030303030303030227D
\ No newline at end of file

From 944447001789bad56c64241ee61d41309bc28335 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 4 Dec 2024 12:19:24 -0500
Subject: [PATCH 25/39] remove static folder

---
 .well-known/apple-developer-merchantid-domain-association | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 .well-known/apple-developer-merchantid-domain-association

diff --git a/.well-known/apple-developer-merchantid-domain-association b/.well-known/apple-developer-merchantid-domain-association
new file mode 100644
index 0000000000..2ff95c9628
--- /dev/null
+++ b/.well-known/apple-developer-merchantid-domain-association
@@ -0,0 +1 @@
+7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313536363233343735303036312C227369676E6174757265223A22333038303036303932613836343838366637306430313037303261303830333038303032303130313331306633303064303630393630383634383031363530333034303230313035303033303830303630393261383634383836663730643031303730313030303061303830333038323033653333303832303338386130303330323031303230323038346333303431343935313964353433363330306130363038326138363438636533643034303330323330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313339333033353331333833303331333333323335333735613137306433323334333033353331333633303331333333323335333735613330356633313235333032333036303335353034303330633163363536333633326437333664373032643632373236663662363537323264373336393637366535663535343333343264353035323466343433313134333031323036303335353034306230633062363934663533323035333739373337343635366437333331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034633231353737656465626436633762323231386636386464373039306131323138646337623062643666326332383364383436303935643934616634613534313162383334323065643831316633343037653833333331663163353463336637656233323230643662616435643465666634393238393839336537633066313361333832303231313330383230323064333030633036303335353164313330313031666630343032333030303330316630363033353531643233303431383330313638303134323366323439633434663933653465663237653663346636323836633366613262626664326534623330343530363038326230363031303530353037303130313034333933303337333033353036303832623036303130353035303733303031383632393638373437343730336132663266366636333733373032653631373037303663363532653633366636643266366636333733373033303334326436313730373036633635363136393633363133333330333233303832303131643036303335353164323030343832303131343330383230313130333038323031306330363039326138363438383666373633363430353031333038316665333038316333303630383262303630313035303530373032303233303831623630633831623335323635366336393631366536333635323036663665323037343638363937333230363336353732373436393636363936333631373436353230363237393230363136653739323037303631373237343739323036313733373337353664363537333230363136333633363537303734363136653633363532303666363632303734363836353230373436383635366532303631373037303663363936333631363236633635323037333734363136653634363137323634323037343635373236643733323036313665363432303633366636653634363937343639366636653733323036663636323037353733363532633230363336353732373436393636363936333631373436353230373036663663363936333739323036313665363432303633363537323734363936363639363336313734363936663665323037303732363136333734363936333635323037333734363137343635366436353665373437333265333033363036303832623036303130353035303730323031313632613638373437343730336132663266373737373737326536313730373036633635326536333666366432663633363537323734363936363639363336313734363536313735373436383666373236393734373932663330333430363033353531643166303432643330326233303239613032376130323538363233363837343734373033613266326636333732366332653631373037303663363532653633366636643266363137303730366336353631363936333631333332653633373236633330316430363033353531643065303431363034313439343537646236666435373438313836383938393736326637653537383530376537396235383234333030653036303335353164306630313031666630343034303330323037383033303066303630393261383634383836663736333634303631643034303230353030333030613036303832613836343863653364303430333032303334393030333034363032323130306265303935373166653731653165373335623535653561666163623463373266656234343566333031383532323263373235313030326236316562643666353530323231303064313862333530613564643664643665623137343630333562313165623263653837636661336536616636636264383338303839306463383263646461613633333038323032656533303832303237356130303330323031303230323038343936643266626633613938646139373330306130363038326138363438636533643034303330323330363733313162333031393036303335353034303330633132343137303730366336353230353236663666373432303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330316531373064333133343330333533303336333233333334333633333330356131373064333233393330333533303336333233333334333633333330356133303761333132653330326330363033353530343033306332353431373037303663363532303431373037303663363936333631373436393666366532303439366537343635363737323631373436393666366532303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330353933303133303630373261383634386365336430323031303630383261383634386365336430333031303730333432303030346630313731313834313964373634383564353161356532353831303737366538383061326566646537626165346465303864666334623933653133333536643536363562333561653232643039373736306432323465376262613038666437363137636538386362373662623636373062656338653832393834666635343435613338316637333038316634333034363036303832623036303130353035303730313031303433613330333833303336303630383262303630313035303530373330303138363261363837343734373033613266326636663633373337303265363137303730366336353265363336663664326636663633373337303330333432643631373037303663363537323666366637343633363136373333333031643036303335353164306530343136303431343233663234396334346639336534656632376536633466363238366333666132626266643265346233303066303630333535316431333031303166663034303533303033303130316666333031663036303335353164323330343138333031363830313462626230646561313538333338383961613438613939646562656264656261666461636232346162333033373036303335353164316630343330333032653330326361303261613032383836323636383734373437303361326632663633373236633265363137303730366336353265363336663664326636313730373036633635373236663666373436333631363733333265363337323663333030653036303335353164306630313031666630343034303330323031303633303130303630613261383634383836663736333634303630323065303430323035303033303061303630383261383634386365336430343033303230333637303033303634303233303361636637323833353131363939623138366662333563333536636136326266663431376564643930663735346461323865626566313963383135653432623738396638393866373962353939663938643534313064386639646539633266653032333033323264643534343231623061333035373736633564663333383362393036376664313737633263323136643936346663363732363938323132366635346638376137643162393963623962303938393231363130363939306630393932316430303030333138323031386233303832303138373032303130313330383138363330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533303230383463333034313439353139643534333633303064303630393630383634383031363530333034303230313035303061303831393533303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333933303338333133393331333733313332333333303561333032613036303932613836343838366637306430313039333433313164333031623330306430363039363038363438303136353033303430323031303530306131306130363038326138363438636533643034303330323330326630363039326138363438383666373064303130393034333132323034323062303731303365313430613462386231376262613230316130336163643036396234653431366232613263383066383661383338313435633239373566633131333030613036303832613836343863653364303430333032303434363330343430323230343639306264636637626461663833636466343934396534633035313039656463663334373665303564373261313264376335666538633033303033343464663032323032363764353863393365626233353031333836363062353730373938613064643731313734316262353864626436613138363633353038353431656565393035303030303030303030303030227D
\ No newline at end of file

From 060b13adf3c2ba37aa83b613b3a30d9647008984 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 4 Dec 2024 12:20:38 -0500
Subject: [PATCH 26/39] actually delete the static folder

---
 static/.well-known/apple-developer-merchantid-domain-association | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 static/.well-known/apple-developer-merchantid-domain-association

diff --git a/static/.well-known/apple-developer-merchantid-domain-association b/static/.well-known/apple-developer-merchantid-domain-association
deleted file mode 100644
index 2ff95c9628..0000000000
--- a/static/.well-known/apple-developer-merchantid-domain-association
+++ /dev/null
@@ -1 +0,0 @@
-7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313536363233343735303036312C227369676E6174757265223A22333038303036303932613836343838366637306430313037303261303830333038303032303130313331306633303064303630393630383634383031363530333034303230313035303033303830303630393261383634383836663730643031303730313030303061303830333038323033653333303832303338386130303330323031303230323038346333303431343935313964353433363330306130363038326138363438636533643034303330323330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313339333033353331333833303331333333323335333735613137306433323334333033353331333633303331333333323335333735613330356633313235333032333036303335353034303330633163363536333633326437333664373032643632373236663662363537323264373336393637366535663535343333343264353035323466343433313134333031323036303335353034306230633062363934663533323035333739373337343635366437333331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034633231353737656465626436633762323231386636386464373039306131323138646337623062643666326332383364383436303935643934616634613534313162383334323065643831316633343037653833333331663163353463336637656233323230643662616435643465666634393238393839336537633066313361333832303231313330383230323064333030633036303335353164313330313031666630343032333030303330316630363033353531643233303431383330313638303134323366323439633434663933653465663237653663346636323836633366613262626664326534623330343530363038326230363031303530353037303130313034333933303337333033353036303832623036303130353035303733303031383632393638373437343730336132663266366636333733373032653631373037303663363532653633366636643266366636333733373033303334326436313730373036633635363136393633363133333330333233303832303131643036303335353164323030343832303131343330383230313130333038323031306330363039326138363438383666373633363430353031333038316665333038316333303630383262303630313035303530373032303233303831623630633831623335323635366336393631366536333635323036663665323037343638363937333230363336353732373436393636363936333631373436353230363237393230363136653739323037303631373237343739323036313733373337353664363537333230363136333633363537303734363136653633363532303666363632303734363836353230373436383635366532303631373037303663363936333631363236633635323037333734363136653634363137323634323037343635373236643733323036313665363432303633366636653634363937343639366636653733323036663636323037353733363532633230363336353732373436393636363936333631373436353230373036663663363936333739323036313665363432303633363537323734363936363639363336313734363936663665323037303732363136333734363936333635323037333734363137343635366436353665373437333265333033363036303832623036303130353035303730323031313632613638373437343730336132663266373737373737326536313730373036633635326536333666366432663633363537323734363936363639363336313734363536313735373436383666373236393734373932663330333430363033353531643166303432643330326233303239613032376130323538363233363837343734373033613266326636333732366332653631373037303663363532653633366636643266363137303730366336353631363936333631333332653633373236633330316430363033353531643065303431363034313439343537646236666435373438313836383938393736326637653537383530376537396235383234333030653036303335353164306630313031666630343034303330323037383033303066303630393261383634383836663736333634303631643034303230353030333030613036303832613836343863653364303430333032303334393030333034363032323130306265303935373166653731653165373335623535653561666163623463373266656234343566333031383532323263373235313030326236316562643666353530323231303064313862333530613564643664643665623137343630333562313165623263653837636661336536616636636264383338303839306463383263646461613633333038323032656533303832303237356130303330323031303230323038343936643266626633613938646139373330306130363038326138363438636533643034303330323330363733313162333031393036303335353034303330633132343137303730366336353230353236663666373432303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330316531373064333133343330333533303336333233333334333633333330356131373064333233393330333533303336333233333334333633333330356133303761333132653330326330363033353530343033306332353431373037303663363532303431373037303663363936333631373436393666366532303439366537343635363737323631373436393666366532303433343132303264323034373333333132363330323430363033353530343062306331643431373037303663363532303433363537323734363936363639363336313734363936663665323034313735373436383666373236393734373933313133333031313036303335353034306130633061343137303730366336353230343936653633326533313062333030393036303335353034303631333032353535333330353933303133303630373261383634386365336430323031303630383261383634386365336430333031303730333432303030346630313731313834313964373634383564353161356532353831303737366538383061326566646537626165346465303864666334623933653133333536643536363562333561653232643039373736306432323465376262613038666437363137636538386362373662623636373062656338653832393834666635343435613338316637333038316634333034363036303832623036303130353035303730313031303433613330333833303336303630383262303630313035303530373330303138363261363837343734373033613266326636663633373337303265363137303730366336353265363336663664326636663633373337303330333432643631373037303663363537323666366637343633363136373333333031643036303335353164306530343136303431343233663234396334346639336534656632376536633466363238366333666132626266643265346233303066303630333535316431333031303166663034303533303033303130316666333031663036303335353164323330343138333031363830313462626230646561313538333338383961613438613939646562656264656261666461636232346162333033373036303335353164316630343330333032653330326361303261613032383836323636383734373437303361326632663633373236633265363137303730366336353265363336663664326636313730373036633635373236663666373436333631363733333265363337323663333030653036303335353164306630313031666630343034303330323031303633303130303630613261383634383836663736333634303630323065303430323035303033303061303630383261383634386365336430343033303230333637303033303634303233303361636637323833353131363939623138366662333563333536636136326266663431376564643930663735346461323865626566313963383135653432623738396638393866373962353939663938643534313064386639646539633266653032333033323264643534343231623061333035373736633564663333383362393036376664313737633263323136643936346663363732363938323132366635346638376137643162393963623962303938393231363130363939306630393932316430303030333138323031386233303832303138373032303130313330383138363330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533303230383463333034313439353139643534333633303064303630393630383634383031363530333034303230313035303061303831393533303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333933303338333133393331333733313332333333303561333032613036303932613836343838366637306430313039333433313164333031623330306430363039363038363438303136353033303430323031303530306131306130363038326138363438636533643034303330323330326630363039326138363438383666373064303130393034333132323034323062303731303365313430613462386231376262613230316130336163643036396234653431366232613263383066383661383338313435633239373566633131333030613036303832613836343863653364303430333032303434363330343430323230343639306264636637626461663833636466343934396534633035313039656463663334373665303564373261313264376335666538633033303033343464663032323032363764353863393365626233353031333836363062353730373938613064643731313734316262353864626436613138363633353038353431656565393035303030303030303030303030227D
\ No newline at end of file

From 096edb816280fcfd1b32726a6b57ced654626e82 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 4 Dec 2024 12:53:07 -0500
Subject: [PATCH 27/39] move to public folder

---
 .../.well-known}/apple-developer-merchantid-domain-association    | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename {.well-known => public/.well-known}/apple-developer-merchantid-domain-association (100%)

diff --git a/.well-known/apple-developer-merchantid-domain-association b/public/.well-known/apple-developer-merchantid-domain-association
similarity index 100%
rename from .well-known/apple-developer-merchantid-domain-association
rename to public/.well-known/apple-developer-merchantid-domain-association

From 89a81d8638f513b900ad80d46f5725be26bac898 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 4 Dec 2024 13:42:45 -0500
Subject: [PATCH 28/39] ensure content type is set correctly in server

---
 server/server.js | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/server/server.js b/server/server.js
index 8b4d196451..cfa0c69a15 100644
--- a/server/server.js
+++ b/server/server.js
@@ -125,6 +125,27 @@ if (process.env.BASIC_USERNAME && process.env.BASIC_PASSWORD) {
   );
 }
 
+// routing to serve files in .well-known with specific content type
+// temporary addition for the apple pay integration with donorbox
+app.use(
+  '/.well-known/apple-developer-merchantid-domain-association',
+  (req, res, next) => {
+    const filePath = path.join(
+      __dirname,
+      '../public/.well-known/apple-developer-merchantid-domain-association'
+    );
+
+    res.setHeader('Content-Type', 'text/plain');
+
+    res.sendFile(filePath, (err) => {
+      if (err) {
+        console.error('Error serving file:', err);
+        next(err);
+      }
+    });
+  }
+);
+
 // Body parser, cookie parser, sessions, serve public assets
 app.use(
   '/locales',

From 41b10f9393efc03ef9044720d2a4738d010cc0a6 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 4 Dec 2024 15:09:59 -0500
Subject: [PATCH 29/39] set absolute path

---
 server/server.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/server.js b/server/server.js
index cfa0c69a15..ce33b047d9 100644
--- a/server/server.js
+++ b/server/server.js
@@ -130,7 +130,7 @@ if (process.env.BASIC_USERNAME && process.env.BASIC_PASSWORD) {
 app.use(
   '/.well-known/apple-developer-merchantid-domain-association',
   (req, res, next) => {
-    const filePath = path.join(
+    const filePath = path.resolve(
       __dirname,
       '../public/.well-known/apple-developer-merchantid-domain-association'
     );

From 50077b63f422f511ddb63ab361cd4a8913c0d5c8 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Wed, 4 Dec 2024 15:38:40 -0500
Subject: [PATCH 30/39] explicitly set root path

---
 server/server.js | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/server/server.js b/server/server.js
index ce33b047d9..2226a42749 100644
--- a/server/server.js
+++ b/server/server.js
@@ -130,14 +130,12 @@ if (process.env.BASIC_USERNAME && process.env.BASIC_PASSWORD) {
 app.use(
   '/.well-known/apple-developer-merchantid-domain-association',
   (req, res, next) => {
-    const filePath = path.resolve(
-      __dirname,
-      '../public/.well-known/apple-developer-merchantid-domain-association'
-    );
+    const rootPath = path.resolve(__dirname, '../public/.well-known');
+    const fileName = 'apple-developer-merchantid-domain-association';
 
     res.setHeader('Content-Type', 'text/plain');
 
-    res.sendFile(filePath, (err) => {
+    res.sendFile(fileName, { root: rootPath }, (err) => {
       if (err) {
         console.error('Error serving file:', err);
         next(err);

From 043eb44c23c3265c50edeff191d17539311bf25d Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Thu, 5 Dec 2024 09:30:18 -0500
Subject: [PATCH 31/39] remove apple pay setup in server

---
 server/server.js | 19 -------------------
 1 file changed, 19 deletions(-)

diff --git a/server/server.js b/server/server.js
index 2226a42749..8b4d196451 100644
--- a/server/server.js
+++ b/server/server.js
@@ -125,25 +125,6 @@ if (process.env.BASIC_USERNAME && process.env.BASIC_PASSWORD) {
   );
 }
 
-// routing to serve files in .well-known with specific content type
-// temporary addition for the apple pay integration with donorbox
-app.use(
-  '/.well-known/apple-developer-merchantid-domain-association',
-  (req, res, next) => {
-    const rootPath = path.resolve(__dirname, '../public/.well-known');
-    const fileName = 'apple-developer-merchantid-domain-association';
-
-    res.setHeader('Content-Type', 'text/plain');
-
-    res.sendFile(fileName, { root: rootPath }, (err) => {
-      if (err) {
-        console.error('Error serving file:', err);
-        next(err);
-      }
-    });
-  }
-);
-
 // Body parser, cookie parser, sessions, serve public assets
 app.use(
   '/locales',

From 71fd59e9f15e7f2eac58529012113f0f58974094 Mon Sep 17 00:00:00 2001
From: raclim <raclim@nyu.edu>
Date: Thu, 5 Dec 2024 09:47:15 -0500
Subject: [PATCH 32/39] 2.15.6

---
 package-lock.json | 4 ++--
 package.json      | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 3235c91550..83bed02c1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "p5.js-web-editor",
-  "version": "2.15.5",
+  "version": "2.15.6",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "p5.js-web-editor",
-      "version": "2.15.5",
+      "version": "2.15.6",
       "license": "LGPL-2.1",
       "dependencies": {
         "@auth0/s3": "^1.0.0",
diff --git a/package.json b/package.json
index 8d26a46a4a..5bf70189ea 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "p5.js-web-editor",
-  "version": "2.15.5",
+  "version": "2.15.6",
   "description": "The web editor for p5.js.",
   "scripts": {
     "clean": "rimraf dist",

From d42872fb495e52eb2f9b8968803678e56593feb8 Mon Sep 17 00:00:00 2001
From: raclim <43053081+raclim@users.noreply.github.com>
Date: Fri, 6 Dec 2024 11:30:16 -0500
Subject: [PATCH 33/39] Remove Icon from README.md

---
 README.md | 2 --
 1 file changed, 2 deletions(-)

diff --git a/README.md b/README.md
index 36d7195f88..1314340425 100644
--- a/README.md
+++ b/README.md
@@ -46,5 +46,3 @@ Hosting and technical support has come from:
 <a href="https://releasehub.com/" target="_blank"><img width="100" src="https://assets.website-files.com/603dd147c5b0a480611bd348/603dd147c5b0a469bc1bd451_logo--dark.svg" /></a>
 <br />
 <a href="https://www.browserstack.com/" target="_blank"><img width="100" src="https://user-images.githubusercontent.com/6063380/46976166-ab280a80-d096-11e8-983b-18dd38c8cc9b.png" /></a>
-<br />
-<a href="https://www.fastly.com/" target="_blank"><img width="100" src="https://cdn-assets-us.frontify.com/s3/frontify-enterprise-files-us/eyJwYXRoIjoiZmFzdGx5XC9hY2NvdW50c1wvYzJcLzQwMDEwMjNcL3Byb2plY3RzXC8xMVwvYXNzZXRzXC80ZVwvNzc0XC9lZTZmYzlkOWYzNWE1NjBkNjUzNjFkNGI0NGQ2MTNmZi0xNjIxNTIyODg4LnBuZyJ9:fastly:nVuY3PxyFqQMI6elJsMzxAGLH3IFlmiuMdacHAGRMkE?width=2400" /></a>

From e1a4a97477269460f62889aaf0d7622b78307175 Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Wed, 8 Jan 2025 13:16:29 -0800
Subject: [PATCH 34/39] fix: moved usermenu back into menubar to reenable
 dropdown behavior

---
 client/modules/IDE/components/Header/Nav.jsx | 2 +-
 client/styles/components/_nav.scss           | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx
index 6ddc38bda2..a608c2d214 100644
--- a/client/modules/IDE/components/Header/Nav.jsx
+++ b/client/modules/IDE/components/Header/Nav.jsx
@@ -41,8 +41,8 @@ const Nav = ({ layout }) => {
       <header className="nav__header">
         <Menubar>
           <LeftLayout layout={layout} />
+          <UserMenu />
         </Menubar>
-        <UserMenu />
       </header>
     </>
   );
diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss
index 4ddc96b327..b2dacee5e8 100644
--- a/client/styles/components/_nav.scss
+++ b/client/styles/components/_nav.scss
@@ -3,6 +3,7 @@
 .nav {
   height: #{math.div(42, $base-font-size)}rem;
   display: flex;
+  width: 100%;
   flex-direction: row;
   justify-content: space-between;
 

From 3022bb81637200c36f5b8e5202e0a28ba2331fab Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Wed, 8 Jan 2025 13:35:38 -0800
Subject: [PATCH 35/39] chore: added reference article as comment

---
 client/components/Menubar/MenubarMenu.jsx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/client/components/Menubar/MenubarMenu.jsx b/client/components/Menubar/MenubarMenu.jsx
index 34192b237b..8ad7f4cfef 100644
--- a/client/components/Menubar/MenubarMenu.jsx
+++ b/client/components/Menubar/MenubarMenu.jsx
@@ -1,3 +1,5 @@
+// https://blog.logrocket.com/building-accessible-menubar-component-react
+
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import React, { useContext, useMemo } from 'react';

From d7e2dc1a93ff07547e1ea395fb276dd71a78a29c Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Sat, 11 Jan 2025 17:25:53 -0800
Subject: [PATCH 36/39] fix: removed layout shift on user and example pages
 caused by height:100%

---
 client/styles/components/_nav.scss | 1 -
 1 file changed, 1 deletion(-)

diff --git a/client/styles/components/_nav.scss b/client/styles/components/_nav.scss
index b2dacee5e8..58691ff251 100644
--- a/client/styles/components/_nav.scss
+++ b/client/styles/components/_nav.scss
@@ -16,7 +16,6 @@
   display: flex;
   flex-direction: row;
   justify-content: space-between;
-  height: 100%;
   align-items: center;
 
   @include themify() {

From cca3bd5eaab174f8251285442527e24195c07f46 Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Sat, 11 Jan 2025 18:29:51 -0800
Subject: [PATCH 37/39] refactor: renamed MenubarMenu to MenubarSubmenu for
 clarity

---
 .../Menubar/{MenubarMenu.jsx => MenubarSubmenu.jsx}    | 10 +++++-----
 client/modules/IDE/components/Header/MobileNav.jsx     |  2 +-
 client/modules/IDE/components/Header/Nav.jsx           |  2 +-
 3 files changed, 7 insertions(+), 7 deletions(-)
 rename client/components/Menubar/{MenubarMenu.jsx => MenubarSubmenu.jsx} (94%)

diff --git a/client/components/Menubar/MenubarMenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx
similarity index 94%
rename from client/components/Menubar/MenubarMenu.jsx
rename to client/components/Menubar/MenubarSubmenu.jsx
index 8ad7f4cfef..ae278bdf4f 100644
--- a/client/components/Menubar/MenubarMenu.jsx
+++ b/client/components/Menubar/MenubarSubmenu.jsx
@@ -75,10 +75,10 @@ MenubarList.defaultProps = {
 };
 
 /* -------------------------------------------------------------------------------------------------
- * MenubarMenu
+ * MenubarSubmenu
  * -----------------------------------------------------------------------------------------------*/
 
-function MenubarMenu({ id, title, children }) {
+function MenubarSubmenu({ id, title, children }) {
   const { isOpen } = useMenuProps(id);
 
   return (
@@ -89,14 +89,14 @@ function MenubarMenu({ id, title, children }) {
   );
 }
 
-MenubarMenu.propTypes = {
+MenubarSubmenu.propTypes = {
   id: PropTypes.string.isRequired,
   title: PropTypes.node.isRequired,
   children: PropTypes.node
 };
 
-MenubarMenu.defaultProps = {
+MenubarSubmenu.defaultProps = {
   children: null
 };
 
-export default MenubarMenu;
+export default MenubarSubmenu;
diff --git a/client/modules/IDE/components/Header/MobileNav.jsx b/client/modules/IDE/components/Header/MobileNav.jsx
index c1fd718a8f..349d3d1709 100644
--- a/client/modules/IDE/components/Header/MobileNav.jsx
+++ b/client/modules/IDE/components/Header/MobileNav.jsx
@@ -7,7 +7,7 @@ import { sortBy } from 'lodash';
 import classNames from 'classnames';
 import { ParentMenuContext } from '../../../../components/Menubar/contexts';
 import Menubar from '../../../../components/Menubar/Menubar';
-import { useMenuProps } from '../../../../components/Menubar/MenubarMenu';
+import { useMenuProps } from '../../../../components/Menubar/MenubarSubmenu';
 import NavMenuItem from '../../../../components/Menubar/MenubarItem';
 import { prop, remSize } from '../../../../theme';
 import AsteriskIcon from '../../../../images/p5-asterisk.svg';
diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx
index a608c2d214..a20fe1ac09 100644
--- a/client/modules/IDE/components/Header/Nav.jsx
+++ b/client/modules/IDE/components/Header/Nav.jsx
@@ -4,7 +4,7 @@ import { sortBy } from 'lodash';
 import { Link } from 'react-router-dom';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
-import MenubarMenu from '../../../../components/Menubar/MenubarMenu';
+import MenubarMenu from '../../../../components/Menubar/MenubarSubmenu';
 import MenubarItem from '../../../../components/Menubar/MenubarItem';
 import { availableLanguages, languageKeyToLabel } from '../../../../i18n';
 import getConfig from '../../../../utils/getConfig';

From d4b76729e96cc4a1d9ae55cfea3c78b966bb48b0 Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Sat, 11 Jan 2025 18:48:38 -0800
Subject: [PATCH 38/39] fix: renamed MenubarMenu components to MenubarSubmenu

---
 client/modules/IDE/components/Header/Nav.jsx | 26 ++++++++++----------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx
index a20fe1ac09..95bfaccd85 100644
--- a/client/modules/IDE/components/Header/Nav.jsx
+++ b/client/modules/IDE/components/Header/Nav.jsx
@@ -4,7 +4,7 @@ import { sortBy } from 'lodash';
 import { Link } from 'react-router-dom';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
-import MenubarMenu from '../../../../components/Menubar/MenubarSubmenu';
+import MenubarSubmenu from '../../../../components/Menubar/MenubarSubmenu';
 import MenubarItem from '../../../../components/Menubar/MenubarItem';
 import { availableLanguages, languageKeyToLabel } from '../../../../i18n';
 import getConfig from '../../../../utils/getConfig';
@@ -163,7 +163,7 @@ const ProjectMenu = () => {
           </a>
         )}
       </li>
-      <MenubarMenu id="file" title={t('Nav.File.Title')}>
+      <MenubarSubmenu id="file" title={t('Nav.File.Title')}>
         <MenubarItem onClick={newSketch}>{t('Nav.File.New')}</MenubarItem>
         <MenubarItem
           hideIf={
@@ -208,8 +208,8 @@ const ProjectMenu = () => {
         >
           {t('Nav.File.Examples')}
         </MenubarItem>
-      </MenubarMenu>
-      <MenubarMenu id="edit" title={t('Nav.Edit.Title')}>
+      </MenubarSubmenu>
+      <MenubarSubmenu id="edit" title={t('Nav.Edit.Title')}>
         <MenubarItem onClick={cmRef.current?.tidyCode}>
           {t('Nav.Edit.TidyCode')}
           <span className="nav__keyboard-shortcut">{metaKeyName}+Shift+F</span>
@@ -222,8 +222,8 @@ const ProjectMenu = () => {
           {t('Nav.Edit.Replace')}
           <span className="nav__keyboard-shortcut">{replaceCommand}</span>
         </MenubarItem>
-      </MenubarMenu>
-      <MenubarMenu id="sketch" title={t('Nav.Sketch.Title')}>
+      </MenubarSubmenu>
+      <MenubarSubmenu id="sketch" title={t('Nav.Sketch.Title')}>
         <MenubarItem onClick={() => dispatch(newFile(rootFile.id))}>
           {t('Nav.Sketch.AddFile')}
           <span className="nav__keyboard-shortcut">{newFileCommand}</span>
@@ -241,8 +241,8 @@ const ProjectMenu = () => {
             Shift+{metaKeyName}+Enter
           </span>
         </MenubarItem>
-      </MenubarMenu>
-      <MenubarMenu id="help" title={t('Nav.Help.Title')}>
+      </MenubarSubmenu>
+      <MenubarSubmenu id="help" title={t('Nav.Help.Title')}>
         <MenubarItem onClick={() => dispatch(showKeyboardShortcutModal())}>
           {t('Nav.Help.KeyboardShortcuts')}
         </MenubarItem>
@@ -250,7 +250,7 @@ const ProjectMenu = () => {
           {t('Nav.Help.Reference')}
         </MenubarItem>
         <MenubarItem href="/about">{t('Nav.Help.About')}</MenubarItem>
-      </MenubarMenu>
+      </MenubarSubmenu>
       {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />}
     </ul>
   );
@@ -266,14 +266,14 @@ const LanguageMenu = () => {
   }
 
   return (
-    <MenubarMenu id="lang" title={languageKeyToLabel(language)}>
+    <MenubarSubmenu id="lang" title={languageKeyToLabel(language)}>
       {sortBy(availableLanguages).map((key) => (
         // eslint-disable-next-line react/jsx-no-bind
         <MenubarItem key={key} value={key} onClick={handleLangSelection}>
           {languageKeyToLabel(key)}
         </MenubarItem>
       ))}
-    </MenubarMenu>
+    </MenubarSubmenu>
   );
 };
 
@@ -310,7 +310,7 @@ const AuthenticatedUserMenu = () => {
 
   return (
     <ul className="nav__items-right" title="user-menu">
-      <MenubarMenu
+      <MenubarSubmenu
         id="account"
         title={
           <span>
@@ -334,7 +334,7 @@ const AuthenticatedUserMenu = () => {
         <MenubarItem onClick={() => dispatch(logoutUser())}>
           {t('Nav.Auth.LogOut')}
         </MenubarItem>
-      </MenubarMenu>
+      </MenubarSubmenu>
     </ul>
   );
 };

From de201ffad0024550e859b0657134c255d7b0b282 Mon Sep 17 00:00:00 2001
From: Tristan Espinoza <tristan.m.espinoza@gmail.com>
Date: Sat, 11 Jan 2025 19:43:32 -0800
Subject: [PATCH 39/39] feat: supprt listbox role for language selection menu

---
 client/components/Menubar/MenubarItem.jsx    | 21 +++++--
 client/components/Menubar/MenubarSubmenu.jsx | 59 +++++++++++++++-----
 client/modules/IDE/components/Header/Nav.jsx | 15 ++++-
 3 files changed, 76 insertions(+), 19 deletions(-)

diff --git a/client/components/Menubar/MenubarItem.jsx b/client/components/Menubar/MenubarItem.jsx
index 8c08aa9c03..8d595bb5cd 100644
--- a/client/components/Menubar/MenubarItem.jsx
+++ b/client/components/Menubar/MenubarItem.jsx
@@ -3,7 +3,13 @@ import React, { useContext, useMemo } from 'react';
 import ButtonOrLink from '../../common/ButtonOrLink';
 import { MenubarContext, ParentMenuContext } from './contexts';
 
-function MenubarItem({ hideIf, className, ...rest }) {
+function MenubarItem({
+  hideIf,
+  className,
+  role: customRole,
+  selected,
+  ...rest
+}) {
   const parent = useContext(ParentMenuContext);
 
   const { createMenuItemHandlers } = useContext(MenubarContext);
@@ -17,9 +23,12 @@ function MenubarItem({ hideIf, className, ...rest }) {
     return null;
   }
 
+  const role = customRole || 'menuitem';
+  const ariaSelected = role === 'option' ? { 'aria-selected': selected } : {};
+
   return (
     <li className={className}>
-      <ButtonOrLink {...rest} {...handlers} role="menuitem" />
+      <ButtonOrLink {...rest} {...handlers} {...ariaSelected} role={role} />
     </li>
   );
 }
@@ -32,14 +41,18 @@ MenubarItem.propTypes = {
    * Provides a way to deal with optional items.
    */
   hideIf: PropTypes.bool,
-  className: PropTypes.string
+  className: PropTypes.string,
+  role: PropTypes.oneOf(['menuitem', 'option']),
+  selected: PropTypes.bool
 };
 
 MenubarItem.defaultProps = {
   onClick: null,
   value: null,
   hideIf: false,
-  className: 'nav__dropdown-item'
+  className: 'nav__dropdown-item',
+  role: 'menuitem',
+  selected: false
 };
 
 export default MenubarItem;
diff --git a/client/components/Menubar/MenubarSubmenu.jsx b/client/components/Menubar/MenubarSubmenu.jsx
index ae278bdf4f..13b0e33177 100644
--- a/client/components/Menubar/MenubarSubmenu.jsx
+++ b/client/components/Menubar/MenubarSubmenu.jsx
@@ -25,15 +25,15 @@ export function useMenuProps(id) {
  * MenubarTrigger
  * -----------------------------------------------------------------------------------------------*/
 
-function MenubarTrigger({ id, title, ...props }) {
+function MenubarTrigger({ id, title, role, hasPopup, ...props }) {
   const { isOpen, handlers } = useMenuProps(id);
 
   return (
     <button
       {...handlers}
       {...props}
-      role="menuitem"
-      aria-haspopup="menu"
+      role={role}
+      aria-haspopup={hasPopup}
       aria-expanded={isOpen}
     >
       <span className="nav__item-header">{title}</span>
@@ -48,16 +48,23 @@ function MenubarTrigger({ id, title, ...props }) {
 
 MenubarTrigger.propTypes = {
   id: PropTypes.string.isRequired,
-  title: PropTypes.node.isRequired
+  title: PropTypes.node.isRequired,
+  role: PropTypes.string,
+  hasPopup: PropTypes.oneOf(['menu', 'listbox', 'true'])
+};
+
+MenubarTrigger.defaultProps = {
+  role: 'menuitem',
+  hasPopup: 'menu'
 };
 
 /* -------------------------------------------------------------------------------------------------
  * MenubarList
  * -----------------------------------------------------------------------------------------------*/
 
-function MenubarList({ id, children }) {
+function MenubarList({ id, children, role, ...props }) {
   return (
-    <ul className="nav__dropdown" role="menu">
+    <ul className="nav__dropdown" role={role} {...props}>
       <ParentMenuContext.Provider value={id}>
         {children}
       </ParentMenuContext.Provider>
@@ -67,24 +74,46 @@ function MenubarList({ id, children }) {
 
 MenubarList.propTypes = {
   id: PropTypes.string.isRequired,
-  children: PropTypes.node
+  children: PropTypes.node,
+  role: PropTypes.oneOf(['menu', 'listbox'])
 };
 
 MenubarList.defaultProps = {
-  children: null
+  children: null,
+  role: 'menu'
 };
 
 /* -------------------------------------------------------------------------------------------------
  * MenubarSubmenu
  * -----------------------------------------------------------------------------------------------*/
 
-function MenubarSubmenu({ id, title, children }) {
+function MenubarSubmenu({
+  id,
+  title,
+  children,
+  triggerRole: customTriggerRole,
+  listRole: customListRole,
+  ...props
+}) {
   const { isOpen } = useMenuProps(id);
 
+  const triggerRole = customTriggerRole || 'menuitem';
+  const listRole = customListRole || 'menu';
+
+  const hasPopup = listRole === 'listbox' ? 'listbox' : 'menu';
+
   return (
     <li className={classNames('nav__item', isOpen && 'nav__item--open')}>
-      <MenubarTrigger id={id} title={title} />
-      <MenubarList id={id}>{children}</MenubarList>
+      <MenubarTrigger
+        id={id}
+        title={title}
+        role={triggerRole}
+        hasPopup={hasPopup}
+        {...props}
+      />
+      <MenubarList id={id} role={listRole}>
+        {children}
+      </MenubarList>
     </li>
   );
 }
@@ -92,11 +121,15 @@ function MenubarSubmenu({ id, title, children }) {
 MenubarSubmenu.propTypes = {
   id: PropTypes.string.isRequired,
   title: PropTypes.node.isRequired,
-  children: PropTypes.node
+  children: PropTypes.node,
+  triggerRole: PropTypes.string,
+  listRole: PropTypes.string
 };
 
 MenubarSubmenu.defaultProps = {
-  children: null
+  children: null,
+  triggerRole: 'menuitem',
+  listRole: 'menu'
 };
 
 export default MenubarSubmenu;
diff --git a/client/modules/IDE/components/Header/Nav.jsx b/client/modules/IDE/components/Header/Nav.jsx
index 95bfaccd85..f40e9137bf 100644
--- a/client/modules/IDE/components/Header/Nav.jsx
+++ b/client/modules/IDE/components/Header/Nav.jsx
@@ -266,10 +266,21 @@ const LanguageMenu = () => {
   }
 
   return (
-    <MenubarSubmenu id="lang" title={languageKeyToLabel(language)}>
+    <MenubarSubmenu
+      id="lang"
+      title={languageKeyToLabel(language)}
+      triggerRole="button"
+      listRole="listbox"
+    >
       {sortBy(availableLanguages).map((key) => (
         // eslint-disable-next-line react/jsx-no-bind
-        <MenubarItem key={key} value={key} onClick={handleLangSelection}>
+        <MenubarItem
+          key={key}
+          value={key}
+          onClick={handleLangSelection}
+          role="option"
+          selected={key === language}
+        >
           {languageKeyToLabel(key)}
         </MenubarItem>
       ))}