Repository: gitify-app/gitify
Pull request #1270 (2024-06-18 16:07:06)

refactor: extract sidebar button component


Patch:
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index eee0c2c8..a10b36f5 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -8,11 +8,10 @@ import { type FC, useContext, useMemo } from 'react';
 import { useLocation, useNavigate } from 'react-router-dom';
 import { Logo } from '../components/Logo';
 import { AppContext } from '../context/App';
-import { BUTTON_SIDEBAR_CLASS_NAME } from '../styles/gitify';
-import { cn } from '../utils/cn';
 import { quitApp } from '../utils/comms';
 import { openGitHubNotifications, openGitifyRepository } from '../utils/links';
 import { getNotificationCount } from '../utils/notifications';
+import { SidebarButton } from './buttons/SidebarButton';
 
 export const Sidebar: FC = () => {
   const navigate = useNavigate();
@@ -29,6 +28,11 @@ export const Sidebar: FC = () => {
     }
   };
 
+  const refreshNotifications = () => {
+    navigate('/', { replace: true });
+    fetchNotifications();
+  };
+
   const notificationsCount = useMemo(() => {
     return getNotificationCount(notifications);
   }, [notifications]);
@@ -46,63 +50,42 @@ export const Sidebar: FC = () => {
           <Logo aria-label="Open Gitify" />
         </button>
 
-        <button
-          type="button"
-          className={cn(
-            'my-1 flex cursor-pointer items-center justify-around self-stretch px-2 py-1 text-xs font-extrabold',
-            notificationsCount > 0 ? 'text-green-500' : 'text-white',
-          )}
-          onClick={() => openGitHubNotifications()}
+        <SidebarButton
           title={`${notificationsCount} Unread Notifications`}
-        >
-          <BellIcon
-            size={12}
-            aria-label={`${notificationsCount} Unread Notifications`}
-          />
-          {notificationsCount > 0 && notificationsCount}
-        </button>
+          metric={notificationsCount}
+          icon={BellIcon}
+          onClick={() => openGitHubNotifications()}
+        />
       </div>
 
       <div className="px-3 py-4">
         {isLoggedIn && (
           <>
-            <button
-              type="button"
-              className={BUTTON_SIDEBAR_CLASS_NAME}
+            <SidebarButton
               title="Refresh Notifications"
-              onClick={() => {
-                navigate('/', { replace: true });
-                fetchNotifications();
-              }}
+              icon={SyncIcon}
+              size={16}
+              loading={status === 'loading'}
               disabled={status === 'loading'}
-            >
-              <SyncIcon
-                size={16}
-                aria-label="Refresh Notifications"
-                className={status === 'loading' ? 'animate-spin' : undefined}
-              />
-            </button>
-            <button
-              type="button"
-              className={BUTTON_SIDEBAR_CLASS_NAME}
+              onClick={() => refreshNotifications()}
+            />
+
+            <SidebarButton
               title="Settings"
-              onClick={toggleSettings}
-            >
-              <GearIcon size={16} aria-label="Settings" />
-            </button>
+              icon={GearIcon}
+              size={16}
+              onClick={() => toggleSettings()}
+            />
           </>
         )}
 
         {!isLoggedIn && (
-          <button
-            type="button"
-            className={BUTTON_SIDEBAR_CLASS_NAME}
+          <SidebarButton
             title="Quit Gitify"
-            aria-label="Quit Gitify"
-            onClick={quitApp}
-          >
-            <XCircleIcon size={16} aria-label="Quit Gitify" />
-          </button>
+            icon={XCircleIcon}
+            size={16}
+            onClick={() => quitApp()}
+          />
         )}
       </div>
     </div>
diff --git a/src/components/buttons/SidebarButton.tsx b/src/components/buttons/SidebarButton.tsx
new file mode 100644
index 00000000..26a21672
--- /dev/null
+++ b/src/components/buttons/SidebarButton.tsx
@@ -0,0 +1,38 @@
+import type { Icon } from '@primer/octicons-react';
+import type { FC } from 'react';
+import { IconColor } from '../../types';
+import { cn } from '../../utils/cn';
+
+export interface ISidebarButton {
+  title: string;
+  metric?: number;
+  icon: Icon;
+  onClick?: () => void;
+  size?: number;
+  loading?: boolean;
+  disabled?: boolean;
+}
+
+export const SidebarButton: FC<ISidebarButton> = (props: ISidebarButton) => {
+  const hasMetric = props?.metric > 0;
+
+  return (
+    <button
+      type="button"
+      className={cn(
+        'flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default',
+        hasMetric
+          ? `${IconColor.GREEN} hover:text-green-700`
+          : `${IconColor.WHITE} hover:text-gray-500`,
+        props.loading ? 'animate-spin' : undefined,
+        props.size ? 'py-2' : 'py-1',
+      )}
+      onClick={() => props.onClick()}
+      title={props.title}
+      disabled={props.disabled}
+    >
+      <props.icon size={props.size ?? 12} aria-label={props.title} />
+      {hasMetric && props.metric}
+    </button>
+  );
+};
diff --git a/src/styles/gitify.ts b/src/styles/gitify.ts
index ac91a80b..d6a2e6d4 100644
--- a/src/styles/gitify.ts
+++ b/src/styles/gitify.ts
@@ -1,5 +1,2 @@
 export const BUTTON_CLASS_NAME =
   'hover:text-gray-500 py-1 px-2 my-1 mx-2 focus:outline-none';
-
-export const BUTTON_SIDEBAR_CLASS_NAME =
-  'flex justify-evenly items-center bg-transparent border-0 w-full text-sm text-white my-1 py-2 cursor-pointer hover:text-gray-500 focus:outline-none disabled:text-gray-500 disabled:cursor-default';
diff --git a/src/types.ts b/src/types.ts
index 7b3d1bbd..410c5c95 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -127,6 +127,7 @@ export enum IconColor {
   PURPLE = 'text-purple-500',
   RED = 'text-red-500',
   YELLOW = 'text-yellow-500 dark:text-yellow-300',
+  WHITE = 'text-white',
 }
 
 export type PullRequestApprovalIcon = {


Test patch:
diff --git a/src/components/__snapshots__/Sidebar.test.tsx.snap b/src/components/__snapshots__/Sidebar.test.tsx.snap
index 6ef542d2..4a2dbcf0 100644
--- a/src/components/__snapshots__/Sidebar.test.tsx.snap
+++ b/src/components/__snapshots__/Sidebar.test.tsx.snap
@@ -63,7 +63,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged in)
             </svg>
           </button>
           <button
-            class="my-1 flex cursor-pointer items-center justify-around self-stretch px-2 py-1 text-xs font-extrabold text-green-500"
+            class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-green-500 hover:text-green-700 py-1"
             title="4 Unread Notifications"
             type="button"
           >
@@ -89,8 +89,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged in)
           class="px-3 py-4"
         >
           <button
-            aria-label="Quit Gitify"
-            class="flex justify-evenly items-center bg-transparent border-0 w-full text-sm text-white my-1 py-2 cursor-pointer hover:text-gray-500 focus:outline-none disabled:text-gray-500 disabled:cursor-default"
+            class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-2"
             title="Quit Gitify"
             type="button"
           >
@@ -173,7 +172,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged in)
           </svg>
         </button>
         <button
-          class="my-1 flex cursor-pointer items-center justify-around self-stretch px-2 py-1 text-xs font-extrabold text-green-500"
+          class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-green-500 hover:text-green-700 py-1"
           title="4 Unread Notifications"
           type="button"
         >
@@ -199,8 +198,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged in)
         class="px-3 py-4"
       >
         <button
-          aria-label="Quit Gitify"
-          class="flex justify-evenly items-center bg-transparent border-0 w-full text-sm text-white my-1 py-2 cursor-pointer hover:text-gray-500 focus:outline-none disabled:text-gray-500 disabled:cursor-default"
+          class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-2"
           title="Quit Gitify"
           type="button"
         >
@@ -340,7 +338,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged out)
             </svg>
           </button>
           <button
-            class="my-1 flex cursor-pointer items-center justify-around self-stretch px-2 py-1 text-xs font-extrabold text-green-500"
+            class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-green-500 hover:text-green-700 py-1"
             title="4 Unread Notifications"
             type="button"
           >
@@ -366,8 +364,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged out)
           class="px-3 py-4"
         >
           <button
-            aria-label="Quit Gitify"
-            class="flex justify-evenly items-center bg-transparent border-0 w-full text-sm text-white my-1 py-2 cursor-pointer hover:text-gray-500 focus:outline-none disabled:text-gray-500 disabled:cursor-default"
+            class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-2"
             title="Quit Gitify"
             type="button"
           >
@@ -450,7 +447,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged out)
           </svg>
         </button>
         <button
-          class="my-1 flex cursor-pointer items-center justify-around self-stretch px-2 py-1 text-xs font-extrabold text-green-500"
+          class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-green-500 hover:text-green-700 py-1"
           title="4 Unread Notifications"
           type="button"
         >
@@ -476,8 +473,7 @@ exports[`components/Sidebar.tsx should render itself & its children (logged out)
         class="px-3 py-4"
       >
         <button
-          aria-label="Quit Gitify"
-          class="flex justify-evenly items-center bg-transparent border-0 w-full text-sm text-white my-1 py-2 cursor-pointer hover:text-gray-500 focus:outline-none disabled:text-gray-500 disabled:cursor-default"
+          class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-2"
           title="Quit Gitify"
           type="button"
         >
diff --git a/src/components/buttons/SidebarButton.test.tsx b/src/components/buttons/SidebarButton.test.tsx
new file mode 100644
index 00000000..a5efea9b
--- /dev/null
+++ b/src/components/buttons/SidebarButton.test.tsx
@@ -0,0 +1,25 @@
+import { MarkGithubIcon } from '@primer/octicons-react';
+import { render } from '@testing-library/react';
+import { type ISidebarButton, SidebarButton } from './SidebarButton';
+
+describe('components/buttons/SidebarButton.tsx', () => {
+  it('should render with metric', () => {
+    const props: ISidebarButton = {
+      title: 'Mock Sidebar Button',
+      metric: 1,
+      icon: MarkGithubIcon,
+    };
+    const tree = render(<SidebarButton {...props} />);
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('should render without metric', () => {
+    const props: ISidebarButton = {
+      title: 'Mock Sidebar Button',
+      metric: 0,
+      icon: MarkGithubIcon,
+    };
+    const tree = render(<SidebarButton {...props} />);
+    expect(tree).toMatchSnapshot();
+  });
+});
diff --git a/src/components/buttons/__snapshots__/SidebarButton.test.tsx.snap b/src/components/buttons/__snapshots__/SidebarButton.test.tsx.snap
new file mode 100644
index 00000000..30add36d
--- /dev/null
+++ b/src/components/buttons/__snapshots__/SidebarButton.test.tsx.snap
@@ -0,0 +1,213 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/buttons/SidebarButton.tsx should render with metric 1`] = `
+{
+  "asFragment": [Function],
+  "baseElement": <body>
+    <div>
+      <button
+        class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-green-500 hover:text-green-700 py-1"
+        title="Mock Sidebar Button"
+        type="button"
+      >
+        <svg
+          aria-label="Mock Sidebar Button"
+          class="octicon octicon-mark-github"
+          fill="currentColor"
+          focusable="false"
+          height="12"
+          role="img"
+          style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+          viewBox="0 0 16 16"
+          width="12"
+        >
+          <path
+            d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
+          />
+        </svg>
+        1
+      </button>
+    </div>
+  </body>,
+  "container": <div>
+    <button
+      class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-green-500 hover:text-green-700 py-1"
+      title="Mock Sidebar Button"
+      type="button"
+    >
+      <svg
+        aria-label="Mock Sidebar Button"
+        class="octicon octicon-mark-github"
+        fill="currentColor"
+        focusable="false"
+        height="12"
+        role="img"
+        style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+        viewBox="0 0 16 16"
+        width="12"
+      >
+        <path
+          d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
+        />
+      </svg>
+      1
+    </button>
+  </div>,
+  "debug": [Function],
+  "findAllByAltText": [Function],
+  "findAllByDisplayValue": [Function],
+  "findAllByLabelText": [Function],
+  "findAllByPlaceholderText": [Function],
+  "findAllByRole": [Function],
+  "findAllByTestId": [Function],
+  "findAllByText": [Function],
+  "findAllByTitle": [Function],
+  "findByAltText": [Function],
+  "findByDisplayValue": [Function],
+  "findByLabelText": [Function],
+  "findByPlaceholderText": [Function],
+  "findByRole": [Function],
+  "findByTestId": [Function],
+  "findByText": [Function],
+  "findByTitle": [Function],
+  "getAllByAltText": [Function],
+  "getAllByDisplayValue": [Function],
+  "getAllByLabelText": [Function],
+  "getAllByPlaceholderText": [Function],
+  "getAllByRole": [Function],
+  "getAllByTestId": [Function],
+  "getAllByText": [Function],
+  "getAllByTitle": [Function],
+  "getByAltText": [Function],
+  "getByDisplayValue": [Function],
+  "getByLabelText": [Function],
+  "getByPlaceholderText": [Function],
+  "getByRole": [Function],
+  "getByTestId": [Function],
+  "getByText": [Function],
+  "getByTitle": [Function],
+  "queryAllByAltText": [Function],
+  "queryAllByDisplayValue": [Function],
+  "queryAllByLabelText": [Function],
+  "queryAllByPlaceholderText": [Function],
+  "queryAllByRole": [Function],
+  "queryAllByTestId": [Function],
+  "queryAllByText": [Function],
+  "queryAllByTitle": [Function],
+  "queryByAltText": [Function],
+  "queryByDisplayValue": [Function],
+  "queryByLabelText": [Function],
+  "queryByPlaceholderText": [Function],
+  "queryByRole": [Function],
+  "queryByTestId": [Function],
+  "queryByText": [Function],
+  "queryByTitle": [Function],
+  "rerender": [Function],
+  "unmount": [Function],
+}
+`;
+
+exports[`components/buttons/SidebarButton.tsx should render without metric 1`] = `
+{
+  "asFragment": [Function],
+  "baseElement": <body>
+    <div>
+      <button
+        class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+        title="Mock Sidebar Button"
+        type="button"
+      >
+        <svg
+          aria-label="Mock Sidebar Button"
+          class="octicon octicon-mark-github"
+          fill="currentColor"
+          focusable="false"
+          height="12"
+          role="img"
+          style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+          viewBox="0 0 16 16"
+          width="12"
+        >
+          <path
+            d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
+          />
+        </svg>
+      </button>
+    </div>
+  </body>,
+  "container": <div>
+    <button
+      class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+      title="Mock Sidebar Button"
+      type="button"
+    >
+      <svg
+        aria-label="Mock Sidebar Button"
+        class="octicon octicon-mark-github"
+        fill="currentColor"
+        focusable="false"
+        height="12"
+        role="img"
+        style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+        viewBox="0 0 16 16"
+        width="12"
+      >
+        <path
+          d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
+        />
+      </svg>
+    </button>
+  </div>,
+  "debug": [Function],
+  "findAllByAltText": [Function],
+  "findAllByDisplayValue": [Function],
+  "findAllByLabelText": [Function],
+  "findAllByPlaceholderText": [Function],
+  "findAllByRole": [Function],
+  "findAllByTestId": [Function],
+  "findAllByText": [Function],
+  "findAllByTitle": [Function],
+  "findByAltText": [Function],
+  "findByDisplayValue": [Function],
+  "findByLabelText": [Function],
+  "findByPlaceholderText": [Function],
+  "findByRole": [Function],
+  "findByTestId": [Function],
+  "findByText": [Function],
+  "findByTitle": [Function],
+  "getAllByAltText": [Function],
+  "getAllByDisplayValue": [Function],
+  "getAllByLabelText": [Function],
+  "getAllByPlaceholderText": [Function],
+  "getAllByRole": [Function],
+  "getAllByTestId": [Function],
+  "getAllByText": [Function],
+  "getAllByTitle": [Function],
+  "getByAltText": [Function],
+  "getByDisplayValue": [Function],
+  "getByLabelText": [Function],
+  "getByPlaceholderText": [Function],
+  "getByRole": [Function],
+  "getByTestId": [Function],
+  "getByText": [Function],
+  "getByTitle": [Function],
+  "queryAllByAltText": [Function],
+  "queryAllByDisplayValue": [Function],
+  "queryAllByLabelText": [Function],
+  "queryAllByPlaceholderText": [Function],
+  "queryAllByRole": [Function],
+  "queryAllByTestId": [Function],
+  "queryAllByText": [Function],
+  "queryAllByTitle": [Function],
+  "queryByAltText": [Function],
+  "queryByDisplayValue": [Function],
+  "queryByLabelText": [Function],
+  "queryByPlaceholderText": [Function],
+  "queryByRole": [Function],
+  "queryByTestId": [Function],
+  "queryByText": [Function],
+  "queryByTitle": [Function],
+  "rerender": [Function],
+  "unmount": [Function],
+}
+`;