From 7c4a8216ce8e843640f057a99fbc180e63564df2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Thu, 7 Dec 2023 03:58:21 +0100
Subject: [PATCH] feat: add modal

---
 .../Modal/Modal.component.test.tsx            | 85 +++++++++++++++++++
 .../FunctionalArea/Modal/Modal.component.tsx  | 39 +++++++++
 .../FunctionalArea/Modal/Modal.constants.ts   |  1 +
 .../OverviewImagesModal.component.tsx         |  5 ++
 .../Modal/OverviewImagesModal/index.ts        |  1 +
 src/components/FunctionalArea/Modal/index.ts  |  1 +
 src/components/Map/Map.component.tsx          |  2 +-
 .../MapAdditionalOptions.component.tsx        | 24 ++++--
 src/components/SPA/MinervaSPA.component.tsx   |  2 +
 src/redux/modal/modal.constants.ts            |  7 ++
 src/redux/modal/modal.mock.ts                 |  7 ++
 src/redux/modal/modal.reducers.ts             | 19 +++++
 src/redux/modal/modal.selector.ts             |  6 ++
 src/redux/modal/modal.slice.ts                | 21 +++++
 src/redux/modal/modal.types.ts                |  7 ++
 src/redux/root/root.fixtures.ts               |  2 +
 src/redux/store.ts                            |  2 +
 src/types/modal.ts                            |  1 +
 tailwind.config.ts                            |  1 +
 19 files changed, 227 insertions(+), 6 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/Modal.component.test.tsx
 create mode 100644 src/components/FunctionalArea/Modal/Modal.component.tsx
 create mode 100644 src/components/FunctionalArea/Modal/Modal.constants.ts
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
 create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/index.ts
 create mode 100644 src/components/FunctionalArea/Modal/index.ts
 create mode 100644 src/redux/modal/modal.constants.ts
 create mode 100644 src/redux/modal/modal.mock.ts
 create mode 100644 src/redux/modal/modal.reducers.ts
 create mode 100644 src/redux/modal/modal.selector.ts
 create mode 100644 src/redux/modal/modal.slice.ts
 create mode 100644 src/redux/modal/modal.types.ts
 create mode 100644 src/types/modal.ts

diff --git a/src/components/FunctionalArea/Modal/Modal.component.test.tsx b/src/components/FunctionalArea/Modal/Modal.component.test.tsx
new file mode 100644
index 00000000..62644f66
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/Modal.component.test.tsx
@@ -0,0 +1,85 @@
+import { MODAL_INITIAL_STATE } from '@/redux/modal/modal.constants';
+import { modalSelector } from '@/redux/modal/modal.selector';
+import { StoreType } from '@/redux/store';
+import {
+  InitialStoreState,
+  getReduxWrapperWithStore,
+} from '@/utils/testing/getReduxWrapperWithStore';
+import { render, screen } from '@testing-library/react';
+import { Modal } from './Modal.component';
+
+const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
+  return (
+    render(
+      <Wrapper>
+        <Modal />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
+
+describe('Modal - Component', () => {
+  describe('when modal is hidden', () => {
+    beforeEach(() => {
+      renderComponent({
+        modal: {
+          ...MODAL_INITIAL_STATE,
+          isOpen: false,
+          modalTitle: 'Modal Hidden Title',
+        },
+      });
+    });
+
+    it('should modal have hidden class', () => {
+      const modalElement = screen.getByRole('modal');
+
+      expect(modalElement).toBeInTheDocument();
+      expect(modalElement).toHaveClass('hidden');
+    });
+  });
+
+  describe('when modal is shown', () => {
+    let store: StoreType;
+
+    beforeEach(() => {
+      const { store: newStore } = renderComponent({
+        modal: {
+          ...MODAL_INITIAL_STATE,
+          isOpen: true,
+          modalTitle: 'Modal Opened Title',
+        },
+      });
+
+      store = newStore;
+    });
+
+    it('should modal NOT have hidden class', () => {
+      const modalElement = screen.getByRole('modal');
+
+      expect(modalElement).toBeInTheDocument();
+      expect(modalElement).not.toHaveClass('hidden');
+    });
+
+    it('shows modal title', () => {
+      expect(screen.getByText('Modal Opened Title', { exact: false })).toBeInTheDocument();
+    });
+
+    it('shows modal close button', () => {
+      expect(screen.getByLabelText('close button')).toBeInTheDocument();
+    });
+
+    it('closes modal on close button click', () => {
+      const closeButton = screen.getByLabelText('close button');
+
+      closeButton.click();
+
+      const { isOpen } = modalSelector(store.getState());
+
+      expect(isOpen).toBeFalsy();
+    });
+  });
+});
diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx
new file mode 100644
index 00000000..1a0f2f38
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/Modal.component.tsx
@@ -0,0 +1,39 @@
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { modalSelector } from '@/redux/modal/modal.selector';
+import { closeModal } from '@/redux/modal/modal.slice';
+import { Icon } from '@/shared/Icon';
+import { twMerge } from 'tailwind-merge';
+import { MODAL_ROLE } from './Modal.constants';
+import { OverviewImagesModal } from './OverviewImagesModal';
+
+export const Modal = (): React.ReactNode => {
+  const dispatch = useAppDispatch();
+  const { isOpen, modalName, modalTitle } = useAppSelector(modalSelector);
+
+  const handleCloseModal = (): void => {
+    dispatch(closeModal());
+  };
+
+  return (
+    <div
+      className={twMerge(
+        'absolute left-0 top-0 z-10 h-full w-full bg-cetacean-blue/[.48]',
+        isOpen ? '' : 'hidden',
+      )}
+      role={MODAL_ROLE}
+    >
+      <div className="flex h-full w-full items-center justify-center">
+        <div className="flex flex-col overflow-hidden	rounded-lg	">
+          <div className="flex items-center justify-between bg-white p-[24px] text-xl">
+            <div>{modalTitle}</div>
+            <button type="button" onClick={handleCloseModal} aria-label="close button">
+              <Icon name="close" className="fill-font-500" />
+            </button>
+          </div>
+          <div>{isOpen && modalName === 'overview-images' && <OverviewImagesModal />}</div>
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/src/components/FunctionalArea/Modal/Modal.constants.ts b/src/components/FunctionalArea/Modal/Modal.constants.ts
new file mode 100644
index 00000000..b31cdcfb
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/Modal.constants.ts
@@ -0,0 +1 @@
+export const MODAL_ROLE = 'modal';
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
new file mode 100644
index 00000000..29c97f24
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx
@@ -0,0 +1,5 @@
+import * as React from 'react';
+
+export const OverviewImagesModal: React.FC = () => {
+  return <div className="h-[200px] w-[500px] bg-white " />;
+};
diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/index.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/index.ts
new file mode 100644
index 00000000..ac977070
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/index.ts
@@ -0,0 +1 @@
+export { OverviewImagesModal } from './OverviewImagesModal.component';
diff --git a/src/components/FunctionalArea/Modal/index.ts b/src/components/FunctionalArea/Modal/index.ts
new file mode 100644
index 00000000..b0252805
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/index.ts
@@ -0,0 +1 @@
+export { Modal } from './Modal.component';
diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx
index 27cfcff4..90492655 100644
--- a/src/components/Map/Map.component.tsx
+++ b/src/components/Map/Map.component.tsx
@@ -1,6 +1,6 @@
 import { Drawer } from '@/components/Map/Drawer';
-import { MapViewer } from './MapViewer/MapViewer.component';
 import { MapAdditionalOptions } from './MapAdditionalOptions';
+import { MapViewer } from './MapViewer/MapViewer.component';
 
 export const Map = (): JSX.Element => (
   <div className="relative z-0 h-screen w-full bg-black" data-testid="map-container">
diff --git a/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx b/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx
index a7845ac5..1435e558 100644
--- a/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx
+++ b/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx
@@ -1,10 +1,24 @@
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { openOverviewImagesModal } from '@/redux/modal/modal.slice';
+import { Button } from '@/shared/Button';
 import { twMerge } from 'tailwind-merge';
 import { BackgroundSelector } from './BackgroundsSelector';
 
 // top-[calc(64px+40px+24px)] -> TOP_BAR_HEIGHT+MAP_NAVIGATION_HEIGHT+DISTANCE_FROM_MAP_NAVIGATION
 
-export const MapAdditionalOptions = (): JSX.Element => (
-  <div className={twMerge('absolute right-6 top-[calc(64px+40px+24px)] z-10')}>
-    <BackgroundSelector />
-  </div>
-);
+export const MapAdditionalOptions = (): JSX.Element => {
+  const dispatch = useAppDispatch();
+
+  const handleBrowseOverviewImagesClick = (): void => {
+    dispatch(openOverviewImagesModal());
+  };
+
+  return (
+    <div className={twMerge('absolute right-6 top-[calc(64px+40px+24px)] z-10 flex')}>
+      <Button className="mr-4" onClick={handleBrowseOverviewImagesClick}>
+        Browse overview images
+      </Button>
+      <BackgroundSelector />
+    </div>
+  );
+};
diff --git a/src/components/SPA/MinervaSPA.component.tsx b/src/components/SPA/MinervaSPA.component.tsx
index 3376b1ca..ebcd9438 100644
--- a/src/components/SPA/MinervaSPA.component.tsx
+++ b/src/components/SPA/MinervaSPA.component.tsx
@@ -4,6 +4,7 @@ import { manrope } from '@/constants/font';
 import { useReduxBusQueryManager } from '@/utils/query-manager/useReduxBusQueryManager';
 import { twMerge } from 'tailwind-merge';
 import { useInitializeStore } from '../../utils/initialize/useInitializeStore';
+import { Modal } from '../FunctionalArea/Modal';
 
 export const MinervaSPA = (): JSX.Element => {
   useInitializeStore();
@@ -13,6 +14,7 @@ export const MinervaSPA = (): JSX.Element => {
     <div className={twMerge('relative', manrope.variable)}>
       <FunctionalArea />
       <Map />
+      <Modal />
     </div>
   );
 };
diff --git a/src/redux/modal/modal.constants.ts b/src/redux/modal/modal.constants.ts
new file mode 100644
index 00000000..90ee066d
--- /dev/null
+++ b/src/redux/modal/modal.constants.ts
@@ -0,0 +1,7 @@
+import { ModalState } from './modal.types';
+
+export const MODAL_INITIAL_STATE: ModalState = {
+  isOpen: false,
+  modalName: 'none',
+  modalTitle: '',
+};
diff --git a/src/redux/modal/modal.mock.ts b/src/redux/modal/modal.mock.ts
new file mode 100644
index 00000000..f145fba0
--- /dev/null
+++ b/src/redux/modal/modal.mock.ts
@@ -0,0 +1,7 @@
+import { ModalState } from './modal.types';
+
+export const MODAL_INITIAL_STATE_MOCK: ModalState = {
+  isOpen: false,
+  modalName: 'none',
+  modalTitle: '',
+};
diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts
new file mode 100644
index 00000000..17687aca
--- /dev/null
+++ b/src/redux/modal/modal.reducers.ts
@@ -0,0 +1,19 @@
+import { ModalName } from '@/types/modal';
+import { PayloadAction } from '@reduxjs/toolkit';
+import { ModalState } from './modal.types';
+
+export const openModalReducer = (state: ModalState, action: PayloadAction<ModalName>): void => {
+  state.isOpen = true;
+  state.modalName = action.payload;
+};
+
+export const closeModalReducer = (state: ModalState): void => {
+  state.isOpen = false;
+  state.modalName = 'none';
+};
+
+export const openOverviewImagesModalReducer = (state: ModalState): void => {
+  state.isOpen = true;
+  state.modalName = 'overview-images';
+  state.modalTitle = 'Overview images';
+};
diff --git a/src/redux/modal/modal.selector.ts b/src/redux/modal/modal.selector.ts
new file mode 100644
index 00000000..6221d93c
--- /dev/null
+++ b/src/redux/modal/modal.selector.ts
@@ -0,0 +1,6 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { rootSelector } from '../root/root.selectors';
+
+export const modalSelector = createSelector(rootSelector, state => state.modal);
+
+export const isModalOpenSelector = createSelector(modalSelector, state => state.isOpen);
diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts
new file mode 100644
index 00000000..9ba21287
--- /dev/null
+++ b/src/redux/modal/modal.slice.ts
@@ -0,0 +1,21 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { MODAL_INITIAL_STATE } from './modal.constants';
+import {
+  closeModalReducer,
+  openModalReducer,
+  openOverviewImagesModalReducer,
+} from './modal.reducers';
+
+const modalSlice = createSlice({
+  name: 'modal',
+  initialState: MODAL_INITIAL_STATE,
+  reducers: {
+    openModal: openModalReducer,
+    closeModal: closeModalReducer,
+    openOverviewImagesModal: openOverviewImagesModalReducer,
+  },
+});
+
+export const { openModal, closeModal, openOverviewImagesModal } = modalSlice.actions;
+
+export default modalSlice.reducer;
diff --git a/src/redux/modal/modal.types.ts b/src/redux/modal/modal.types.ts
new file mode 100644
index 00000000..77b0e71f
--- /dev/null
+++ b/src/redux/modal/modal.types.ts
@@ -0,0 +1,7 @@
+import { ModalName } from '@/types/modal';
+
+export interface ModalState {
+  isOpen: boolean;
+  modalName: ModalName;
+  modalTitle: string;
+}
diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts
index bf70be50..0d592254 100644
--- a/src/redux/root/root.fixtures.ts
+++ b/src/redux/root/root.fixtures.ts
@@ -4,6 +4,7 @@ import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock';
 import { initialStateFixture as drawerInitialStateMock } from '../drawer/drawerFixture';
 import { DRUGS_INITIAL_STATE_MOCK } from '../drugs/drugs.mock';
 import { initialMapStateFixture } from '../map/map.fixtures';
+import { MODAL_INITIAL_STATE_MOCK } from '../modal/modal.mock';
 import { MODELS_INITIAL_STATE_MOCK } from '../models/models.mock';
 import { OVERLAYS_INITIAL_STATE_MOCK } from '../overlays/overlays.mock';
 import { PROJECT_STATE_INITIAL_MOCK } from '../project/project.mock';
@@ -23,4 +24,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
   map: initialMapStateFixture,
   overlays: OVERLAYS_INITIAL_STATE_MOCK,
   reactions: REACTIONS_STATE_INITIAL_MOCK,
+  modal: MODAL_INITIAL_STATE_MOCK,
 };
diff --git a/src/redux/store.ts b/src/redux/store.ts
index d98c85c3..e60dd6df 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -4,6 +4,7 @@ import chemicalsReducer from '@/redux/chemicals/chemicals.slice';
 import drawerReducer from '@/redux/drawer/drawer.slice';
 import drugsReducer from '@/redux/drugs/drugs.slice';
 import mapReducer from '@/redux/map/map.slice';
+import modalReducer from '@/redux/modal/modal.slice';
 import modelsReducer from '@/redux/models/models.slice';
 import overlaysReducer from '@/redux/overlays/overlays.slice';
 import projectReducer from '@/redux/project/project.slice';
@@ -25,6 +26,7 @@ export const reducers = {
   chemicals: chemicalsReducer,
   bioEntity: bioEntityReducer,
   drawer: drawerReducer,
+  modal: modalReducer,
   map: mapReducer,
   backgrounds: backgroundsReducer,
   overlays: overlaysReducer,
diff --git a/src/types/modal.ts b/src/types/modal.ts
new file mode 100644
index 00000000..edfac1fd
--- /dev/null
+++ b/src/types/modal.ts
@@ -0,0 +1 @@
+export type ModalName = 'none' | 'overview-images';
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 62c3f4f7..75ce9dc5 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -28,6 +28,7 @@ const config: Config = {
         orange: '#f48c40',
         purple: '#6400e3',
         pink: '#f1009f',
+        'cetacean-blue': '#070130',
       },
       height: {
         'calc-drawer': 'calc(100% - 104px)',
-- 
GitLab