From b81ef7cc3b25b892b57f5a525fe1eff7478d11f0 Mon Sep 17 00:00:00 2001
From: mateuszmiko <dmastah92@gmail.com>
Date: Wed, 4 Oct 2023 11:13:45 +0200
Subject: [PATCH] feat(drawer): add drawer functionality to store

---
 .eslintrc.json                                |  3 +-
 .../MapNavigation/MapNavigation.component.tsx | 11 ++-
 .../NavBar/NavBar.component.test.tsx          |  5 +-
 .../NavBar/NavBar.component.tsx               | 76 ++++++++++++-------
 .../TopBar/TopBar.component.tsx               |  2 +-
 .../Map/Drawer/Drawer.component.test.tsx      | 15 +++-
 .../Map/Drawer/Drawer.component.tsx           | 70 ++++++++---------
 src/components/Map/Drawer/Drawer.constants.ts |  3 +
 src/redux/drawer/drawer.reducers.test.ts      | 60 +++++++++++++++
 src/redux/drawer/drawer.reducers.ts           | 13 ++++
 src/redux/drawer/drawer.selectors.ts          |  4 +
 src/redux/drawer/drawer.slice.ts              | 21 +++++
 src/redux/drawer/drawer.types.ts              |  4 +
 src/redux/root/root.selectors.ts              |  3 +
 src/redux/store.ts                            |  2 +
 src/types/pathName.ts                         |  1 +
 src/utils/renderComponentWithProvider.tsx     |  6 ++
 tailwind.config.ts                            |  3 +
 18 files changed, 234 insertions(+), 68 deletions(-)
 create mode 100644 src/components/Map/Drawer/Drawer.constants.ts
 create mode 100644 src/redux/drawer/drawer.reducers.test.ts
 create mode 100644 src/redux/drawer/drawer.reducers.ts
 create mode 100644 src/redux/drawer/drawer.selectors.ts
 create mode 100644 src/redux/drawer/drawer.slice.ts
 create mode 100644 src/redux/drawer/drawer.types.ts
 create mode 100644 src/redux/root/root.selectors.ts
 create mode 100644 src/types/pathName.ts
 create mode 100644 src/utils/renderComponentWithProvider.tsx

diff --git a/.eslintrc.json b/.eslintrc.json
index c2c5d8be..f7df93bc 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -63,7 +63,8 @@
           "**/*{.,_}{test,spec}.{ts,tsx}", // tests where the extension or filename suffix denotes that it is a test
           "**/jest.config.ts", // jest config
           "**/jest.setup.ts", // jest setup
-          "**/setupTests.ts"
+          "**/setupTests.ts",
+          "src/utils/*.{ts,tsx}"
         ],
         "optionalDependencies": false,
         "peerDependencies": false,
diff --git a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx
index 75808add..9a2e9c9b 100644
--- a/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx
+++ b/src/components/FunctionalArea/MapNavigation/MapNavigation.component.tsx
@@ -1 +1,10 @@
-export const MapNavigation = (): JSX.Element => <div className="h-10 w-full bg-slate-200">.</div>;
+import { Button } from '@/shared/Button';
+
+export const MapNavigation = (): JSX.Element => (
+  <div className="h-10 w-full bg-white-pearl shadow-map-navigation-bar">
+    {/* TODO: Button is temporary until we add tabs */}
+    <Button className="h-10 bg-[#EBF4FF]" variantStyles="secondary">
+      Main map
+    </Button>
+  </div>
+);
diff --git a/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx
index e23f2412..8b38fda6 100644
--- a/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx
+++ b/src/components/FunctionalArea/NavBar/NavBar.component.test.tsx
@@ -1,7 +1,8 @@
-import { screen, render, RenderResult } from '@testing-library/react';
+import { RenderResult, screen } from '@testing-library/react';
+import { renderComponentWithProvider } from '@/utils/renderComponentWithProvider';
 import { NavBar } from './NavBar.component';
 
-const renderComponent = (): RenderResult => render(<NavBar />);
+const renderComponent = (): RenderResult => renderComponentWithProvider(<NavBar />);
 
 describe('NavBar - component', () => {
   it('Should contain navigation buttons and logos with powered by info', () => {
diff --git a/src/components/FunctionalArea/NavBar/NavBar.component.tsx b/src/components/FunctionalArea/NavBar/NavBar.component.tsx
index 91a712ab..14af6635 100644
--- a/src/components/FunctionalArea/NavBar/NavBar.component.tsx
+++ b/src/components/FunctionalArea/NavBar/NavBar.component.tsx
@@ -2,34 +2,56 @@ import Image from 'next/image';
 import { IconButton } from '@/shared/IconButton';
 import logoImg from '@/assets/images/logo.png';
 import luxembourgLogoImg from '@/assets/images/luxembourg-logo.png';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { openDrawer } from '@/redux/drawer/drawer.slice';
 
-export const NavBar = (): JSX.Element => (
-  <div className="flex min-h-full w-[88px] flex-col items-center justify-between bg-cultured py-8">
-    <div data-testid="nav-buttons">
-      <div className="mb-8 flex flex-col gap-[10px]">
-        <IconButton icon="info" />
-        <IconButton icon="page" />
-        <IconButton icon="plugin" />
-        <IconButton icon="export" />
-      </div>
-      <div className="flex flex-col gap-[10px]">
-        <IconButton icon="admin" />
-        <IconButton icon="legend" />
+export const NavBar = (): JSX.Element => {
+  const dispatch = useAppDispatch();
+
+  const openDrawerInfo = (): void => {
+    dispatch(openDrawer('project-info'));
+  };
+
+  const openDrawerPlugins = (): void => {
+    dispatch(openDrawer('plugins'));
+  };
+
+  const openDrawerExport = (): void => {
+    dispatch(openDrawer('export'));
+  };
+
+  const openDrawerLegend = (): void => {
+    dispatch(openDrawer('legend'));
+  };
+
+  return (
+    <div className="flex min-h-full w-[88px] flex-col items-center justify-between bg-cultured py-8">
+      <div data-testid="nav-buttons">
+        <div className="mb-8 flex flex-col gap-[10px]">
+          <IconButton icon="info" onClick={openDrawerInfo} />
+          <IconButton icon="page" />
+          <IconButton icon="plugin" onClick={openDrawerPlugins} />
+          <IconButton icon="export" onClick={openDrawerExport} />
+        </div>
+        <div className="flex flex-col gap-[10px]">
+          <IconButton icon="admin" />
+          <IconButton icon="legend" onClick={openDrawerLegend} />
+        </div>
       </div>
-    </div>
 
-    <div className="flex flex-col items-center gap-[20px]" data-testid="nav-logos-and-powered-by">
-      <Image
-        className="rounded rounded-e rounded-s bg-white-pearl pb-[7px]"
-        src={luxembourgLogoImg}
-        alt="luxembourg logo"
-        height={41}
-        width={48}
-      />
-      <Image src={logoImg} alt="logo" height={48} width={48} />
-      <span className="h-16 w-14 text-center text-[8px] leading-4">
-        Powered by: MINERVA Platform (v16.0.8)
-      </span>
+      <div className="flex flex-col items-center gap-[20px]" data-testid="nav-logos-and-powered-by">
+        <Image
+          className="rounded rounded-e rounded-s bg-white-pearl pb-[7px]"
+          src={luxembourgLogoImg}
+          alt="luxembourg logo"
+          height={41}
+          width={48}
+        />
+        <Image src={logoImg} alt="logo" height={48} width={48} />
+        <span className="h-16 w-14 text-center text-[8px] leading-4">
+          Powered by: MINERVA Platform (v16.0.8)
+        </span>
+      </div>
     </div>
-  </div>
-);
+  );
+};
diff --git a/src/components/FunctionalArea/TopBar/TopBar.component.tsx b/src/components/FunctionalArea/TopBar/TopBar.component.tsx
index e5a94463..83a5c299 100644
--- a/src/components/FunctionalArea/TopBar/TopBar.component.tsx
+++ b/src/components/FunctionalArea/TopBar/TopBar.component.tsx
@@ -3,7 +3,7 @@ import { UserAvatar } from '@/components/FunctionalArea/TopBar/UserAvatar';
 import { Button } from '@/shared/Button';
 
 export const TopBar = (): JSX.Element => (
-  <div className="flex h-16 w-full flex-row items-center justify-between bg-white py-4 pl-7 pr-6">
+  <div className="flex h-16 w-full flex-row items-center justify-between border-b border-font-500 border-opacity-[0.12] bg-white py-4 pl-7 pr-6">
     <div className="flex flex-row items-center">
       <UserAvatar />
       <SearchBar />
diff --git a/src/components/Map/Drawer/Drawer.component.test.tsx b/src/components/Map/Drawer/Drawer.component.test.tsx
index af462cab..20426d94 100644
--- a/src/components/Map/Drawer/Drawer.component.test.tsx
+++ b/src/components/Map/Drawer/Drawer.component.test.tsx
@@ -1,7 +1,8 @@
-import { screen, render, RenderResult } from '@testing-library/react';
+import { screen, fireEvent, type RenderResult } from '@testing-library/react';
+import { renderComponentWithProvider } from '@/utils/renderComponentWithProvider';
 import { Drawer } from './Drawer.component';
 
-const renderComponent = (): RenderResult => render(<Drawer />);
+const renderComponent = (): RenderResult => renderComponentWithProvider(<Drawer />);
 
 describe('Drawer - component', () => {
   it('should render Drawer', () => {
@@ -9,4 +10,14 @@ describe('Drawer - component', () => {
 
     expect(screen.getByRole('drawer')).toBeInTheDocument();
   });
+
+  it('should close Drawer', async () => {
+    renderComponent();
+
+    const button = screen.getByRole('close-drawer-button');
+
+    await fireEvent.click(button);
+
+    expect(screen.getByRole('drawer')).not.toHaveClass('translate-x-0');
+  });
 });
diff --git a/src/components/Map/Drawer/Drawer.component.tsx b/src/components/Map/Drawer/Drawer.component.tsx
index bab06776..47ee77aa 100644
--- a/src/components/Map/Drawer/Drawer.component.tsx
+++ b/src/components/Map/Drawer/Drawer.component.tsx
@@ -1,44 +1,46 @@
-import { useState } from 'react';
-import { Button } from '@/shared/Button';
 import { twMerge } from 'tailwind-merge';
 import { IconButton } from '@/shared/IconButton';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { closeDrawer } from '@/redux/drawer/drawer.slice';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { drawerDataSelector } from '@/redux/drawer/drawer.selectors';
+import {
+  CLOSE_BUTTON_ROLE,
+  DRAWER_ROLE,
+  SOURCE_FROM_DRAWER,
+} from '@/components/Map/Drawer/Drawer.constants';
 
-const drawerRole = 'drawer';
-const closeButtonRole = 'close-drawer-button';
+export const Drawer = (): JSX.Element => {
+  const dispatch = useAppDispatch();
+  const drawerData = useAppSelector(drawerDataSelector);
+  const { open } = drawerData;
 
-export const Drawer = (): JSX.Element | null => {
-  const [open, setOpenDrawer] = useState(false);
+  const handleCloseDrawer = (): void => {
+    // eslint-disable-next-line prefer-template
+    dispatch(closeDrawer(SOURCE_FROM_DRAWER));
+  };
 
   return (
-    <>
-      <Button
-        className="peer absolute left-[100px] top-[110px] z-10"
-        onClick={(): void => setOpenDrawer(true)}
-      >
-        Open Drawer
-      </Button>
-
-      <div
-        className={twMerge(
-          'absolute left-[88px] top-[104px] z-10 h-calc-drawer w-[432px] -translate-x-full transform bg-white-pearl text-font-500 transition-all duration-500',
-          open && 'translate-x-0',
-        )}
-        role={drawerRole}
-      >
-        <div className="flex items-center justify-between border-b border-b-divide px-6 py-8 text-xl">
-          <div>
-            <span className="font-normal">Search: </span>
-            <span className="font-semibold">NADH</span>
-          </div>
-          <IconButton
-            className="bg-white-pearl"
-            classNameIcon="fill-font-500"
-            icon="close"
-            role={closeButtonRole}
-            onClick={(): void => setOpenDrawer(false)}
-          />
+    <div
+      className={twMerge(
+        'absolute left-[88px] top-[104px] z-10 h-calc-drawer w-[432px] -translate-x-full transform bg-white-pearl text-font-500 transition-all duration-500',
+        open && 'translate-x-0',
+      )}
+      role={DRAWER_ROLE}
+    >
+      <div className="flex items-center justify-between border-b border-b-divide px-6 py-8 text-xl">
+        <div>
+          <span className="font-normal">Search: </span>
+          <span className="font-semibold">NADH</span>
         </div>
+        <IconButton
+          className="bg-white-pearl"
+          classNameIcon="fill-font-500"
+          icon="close"
+          role={CLOSE_BUTTON_ROLE}
+          onClick={handleCloseDrawer}
+        />
       </div>
-    </>
+    </div>
   );
 };
diff --git a/src/components/Map/Drawer/Drawer.constants.ts b/src/components/Map/Drawer/Drawer.constants.ts
new file mode 100644
index 00000000..3e3ce70f
--- /dev/null
+++ b/src/components/Map/Drawer/Drawer.constants.ts
@@ -0,0 +1,3 @@
+export const DRAWER_ROLE = 'drawer';
+export const CLOSE_BUTTON_ROLE = 'close-drawer-button';
+export const SOURCE_FROM_DRAWER = 'search';
diff --git a/src/redux/drawer/drawer.reducers.test.ts b/src/redux/drawer/drawer.reducers.test.ts
new file mode 100644
index 00000000..fb360a4f
--- /dev/null
+++ b/src/redux/drawer/drawer.reducers.test.ts
@@ -0,0 +1,60 @@
+import { AnyAction } from '@reduxjs/toolkit';
+import * as toolkitRaw from '@reduxjs/toolkit';
+import type { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore';
+import drawerReducer, { openDrawer, closeDrawer } from './drawer.slice';
+import type { DrawerState } from './drawer.types';
+
+const INITIAL_STATE: DrawerState = {
+  open: false,
+  pathName: 'none',
+};
+
+type SliceReducerType = ToolkitStore<
+  {
+    drawer: DrawerState;
+  },
+  AnyAction
+>;
+
+const createStoreInstanceUsingSliceReducer = (): SliceReducerType =>
+  toolkitRaw.configureStore({
+    reducer: {
+      drawer: drawerReducer,
+    },
+  });
+
+describe('drawer reducer', () => {
+  let store = {} as SliceReducerType;
+
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer();
+  });
+
+  it('should match initial state', () => {
+    const action = { type: 'unknown' };
+
+    expect(drawerReducer(undefined, action)).toEqual(INITIAL_STATE);
+  });
+
+  it('should update the store when you click a project info button on the nav bar', async () => {
+    const { type } = await store.dispatch(openDrawer('project-info'));
+    const { open, pathName } = store.getState().drawer;
+
+    expect(type).toBe('drawer/openDrawer');
+    expect(open).toBe(true);
+    expect(pathName).toEqual('project-info');
+  });
+
+  it('should update the store when you click the close button on the drawer', async () => {
+    const { type } = await store.dispatch(closeDrawer('project-info'));
+    const { open, pathName } = store.getState().drawer;
+
+    expect(type).toBe('drawer/closeDrawer');
+    expect(open).toBe(false);
+    expect(pathName).toEqual('project-info');
+  });
+
+  it.skip('should update the store when you type in the search', async () => {
+    // TODO
+  });
+});
diff --git a/src/redux/drawer/drawer.reducers.ts b/src/redux/drawer/drawer.reducers.ts
new file mode 100644
index 00000000..5cc9f02b
--- /dev/null
+++ b/src/redux/drawer/drawer.reducers.ts
@@ -0,0 +1,13 @@
+import { PayloadAction } from '@reduxjs/toolkit';
+import { DrawerState } from '@/redux/drawer/drawer.types';
+import { PathName } from '@/types/pathName';
+
+export const openDrawerReducer = (state: DrawerState, action: PayloadAction<PathName>): void => {
+  state.open = true;
+  state.pathName = action.payload;
+};
+
+export const closeDrawerReducer = (state: DrawerState, action: PayloadAction<PathName>): void => {
+  state.open = false;
+  state.pathName = action.payload;
+};
diff --git a/src/redux/drawer/drawer.selectors.ts b/src/redux/drawer/drawer.selectors.ts
new file mode 100644
index 00000000..a49b98ff
--- /dev/null
+++ b/src/redux/drawer/drawer.selectors.ts
@@ -0,0 +1,4 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { rootSelector } from '@/redux/root/root.selectors';
+
+export const drawerDataSelector = createSelector(rootSelector, state => state.drawer);
diff --git a/src/redux/drawer/drawer.slice.ts b/src/redux/drawer/drawer.slice.ts
new file mode 100644
index 00000000..42e426de
--- /dev/null
+++ b/src/redux/drawer/drawer.slice.ts
@@ -0,0 +1,21 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { DrawerState } from '@/redux/drawer/drawer.types';
+import { openDrawerReducer, closeDrawerReducer } from './drawer.reducers';
+
+const initialState: DrawerState = {
+  open: false,
+  pathName: 'none',
+};
+
+const drawerSlice = createSlice({
+  name: 'drawer',
+  initialState,
+  reducers: {
+    openDrawer: openDrawerReducer,
+    closeDrawer: closeDrawerReducer,
+  },
+});
+
+export const { openDrawer, closeDrawer } = drawerSlice.actions;
+
+export default drawerSlice.reducer;
diff --git a/src/redux/drawer/drawer.types.ts b/src/redux/drawer/drawer.types.ts
new file mode 100644
index 00000000..b299eaa1
--- /dev/null
+++ b/src/redux/drawer/drawer.types.ts
@@ -0,0 +1,4 @@
+export type DrawerState = {
+  open: boolean;
+  pathName: 'none' | 'search' | 'project-info' | 'plugins' | 'export' | 'legend';
+};
diff --git a/src/redux/root/root.selectors.ts b/src/redux/root/root.selectors.ts
new file mode 100644
index 00000000..7976c8fc
--- /dev/null
+++ b/src/redux/root/root.selectors.ts
@@ -0,0 +1,3 @@
+import type { RootState } from '@/redux/store';
+
+export const rootSelector = (state: RootState): RootState => state;
diff --git a/src/redux/store.ts b/src/redux/store.ts
index d1335018..c385726d 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -1,11 +1,13 @@
 import { configureStore } from '@reduxjs/toolkit';
 import searchReducer from '@/redux/search/search.slice';
 import projectSlice from '@/redux/project/project.slice';
+import drawerReducer from '@/redux/drawer/drawer.slice';
 
 export const store = configureStore({
   reducer: {
     search: searchReducer,
     project: projectSlice,
+    drawer: drawerReducer,
   },
   devTools: true,
 });
diff --git a/src/types/pathName.ts b/src/types/pathName.ts
new file mode 100644
index 00000000..2b61453e
--- /dev/null
+++ b/src/types/pathName.ts
@@ -0,0 +1 @@
+export type PathName = 'none' | 'search' | 'project-info' | 'plugins' | 'export' | 'legend';
diff --git a/src/utils/renderComponentWithProvider.tsx b/src/utils/renderComponentWithProvider.tsx
new file mode 100644
index 00000000..c62bc5d9
--- /dev/null
+++ b/src/utils/renderComponentWithProvider.tsx
@@ -0,0 +1,6 @@
+import { RenderResult, render } from '@testing-library/react';
+import { AppWrapper } from '@/components/AppWrapper';
+import type { ReactNode } from 'react';
+
+export const renderComponentWithProvider = (children: ReactNode): RenderResult =>
+  render(<AppWrapper>{children}</AppWrapper>);
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 4eca2c5b..ce605cba 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -29,6 +29,9 @@ const config: Config = {
       height: {
         'calc-drawer': 'calc(100% - 104px)',
       },
+      boxShadow: {
+        'map-navigation-bar': '4px 8px 32px 0px rgba(0, 0, 0, 0.12)',
+      },
     },
     fontFamily: {
       manrope: ['var(--font-manrope)'],
-- 
GitLab