From be986a762a35f5ac4c0bda1455e6d9078fc631e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tadeusz=20Miesi=C4=85c?= <tadeusz.miesiac@gmail.com>
Date: Fri, 2 Feb 2024 15:19:44 +0100
Subject: [PATCH] feat(publications): search bar && layout modal

---
 .../Modal/Modal.component.test.tsx            | 85 -------------------
 .../FunctionalArea/Modal/Modal.component.tsx  | 56 +++++-------
 .../ModalLayout/ModalLayout.component.tsx     | 44 ++++++++++
 .../ModalLayout.constants.ts}                 |  0
 .../FunctionalArea/Modal/ModalLayout/index.ts |  1 +
 .../PublicationsModal/PublicationsModal.tsx   | 23 ++---
 .../PublicationsModalLayout.component.tsx     | 45 ++++++++++
 .../PublicationsModalLayout.constants.ts      |  1 +
 .../PublicationsModalLayout/index.ts          |  1 +
 .../PublicationsSearch.component.tsx          | 36 ++++++--
 .../FilterBySubmapHeader.component.tsx        | 10 ++-
 .../PublicationsTable.component.tsx           |  9 +-
 .../SortByHeader/SortByHeader.component.tsx   |  9 +-
 src/redux/publications/publications.mock.ts   |  1 +
 .../publications/publications.reducers.ts     |  7 ++
 .../publications/publications.selectors.ts    |  5 ++
 src/redux/publications/publications.slice.ts  |  6 +-
 src/redux/publications/publications.types.ts  |  1 +
 .../LoadingIndicator.component.tsx            | 23 +++++
 src/shared/LoadingIndicator/index.ts          |  1 +
 20 files changed, 208 insertions(+), 156 deletions(-)
 delete mode 100644 src/components/FunctionalArea/Modal/Modal.component.test.tsx
 create mode 100644 src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
 rename src/components/FunctionalArea/Modal/{Modal.constants.ts => ModalLayout/ModalLayout.constants.ts} (100%)
 create mode 100644 src/components/FunctionalArea/Modal/ModalLayout/index.ts
 create mode 100644 src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.component.tsx
 create mode 100644 src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.constants.ts
 create mode 100644 src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/index.ts
 create mode 100644 src/shared/LoadingIndicator/LoadingIndicator.component.tsx
 create mode 100644 src/shared/LoadingIndicator/index.ts

diff --git a/src/components/FunctionalArea/Modal/Modal.component.test.tsx b/src/components/FunctionalArea/Modal/Modal.component.test.tsx
deleted file mode 100644
index 62644f66..00000000
--- a/src/components/FunctionalArea/Modal/Modal.component.test.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-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
index f8f68474..00e22313 100644
--- a/src/components/FunctionalArea/Modal/Modal.component.tsx
+++ b/src/components/FunctionalArea/Modal/Modal.component.tsx
@@ -1,14 +1,10 @@
-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 dynamic from 'next/dynamic';
-import { twMerge } from 'tailwind-merge';
 import { LoginModal } from './LoginModal';
-import { MODAL_ROLE } from './Modal.constants';
 import { OverviewImagesModal } from './OverviewImagesModal';
 import { PublicationsModal } from './PublicationsModal';
+import { ModalLayout } from './ModalLayout';
 
 const MolArtModal = dynamic(
   () => import('./MolArtModal/MolArtModal.component').then(mod => mod.MolArtModal),
@@ -16,40 +12,26 @@ const MolArtModal = dynamic(
 );
 
 export const Modal = (): React.ReactNode => {
-  const dispatch = useAppDispatch();
-  const { isOpen, modalName, modalTitle } = useAppSelector(modalSelector);
-
-  const handleCloseModal = (): void => {
-    dispatch(closeModal());
-  };
+  const { isOpen, modalName } = useAppSelector(modalSelector);
 
   return (
-    <div
-      className={twMerge(
-        'absolute left-0 top-0 z-10 h-full w-full bg-cetacean-blue/[.48]',
-        isOpen ? '' : 'hidden',
+    <>
+      {isOpen && modalName === 'overview-images' && (
+        <ModalLayout>
+          <OverviewImagesModal />
+        </ModalLayout>
+      )}
+      {isOpen && modalName === 'mol-art' && (
+        <ModalLayout>
+          <MolArtModal />
+        </ModalLayout>
+      )}
+      {isOpen && modalName === 'login' && (
+        <ModalLayout>
+          <LoginModal />
+        </ModalLayout>
       )}
-      role={MODAL_ROLE}
-    >
-      <div className="flex h-full w-full items-center justify-center">
-        <div
-          className={twMerge(
-            'flex h-5/6 w-10/12	flex-col	overflow-hidden rounded-lg',
-            modalName === 'login' && 'h-auto w-[400px]',
-          )}
-        >
-          <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>
-          {isOpen && modalName === 'overview-images' && <OverviewImagesModal />}
-          {isOpen && modalName === 'mol-art' && <MolArtModal />}
-          {isOpen && modalName === 'login' && <LoginModal />}
-          {isOpen && modalName === 'publications' && <PublicationsModal />}
-        </div>
-      </div>
-    </div>
+      {isOpen && modalName === 'publications' && <PublicationsModal />}
+    </>
   );
 };
diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
new file mode 100644
index 00000000..792bce0e
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx
@@ -0,0 +1,44 @@
+import { twMerge } from 'tailwind-merge';
+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 { MODAL_ROLE } from './ModalLayout.constants';
+
+type ModalLayoutProps = {
+  children: React.ReactNode;
+};
+
+export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
+  const dispatch = useAppDispatch();
+  const { 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]')}
+      role={MODAL_ROLE}
+    >
+      <div className="flex h-full w-full items-center justify-center">
+        <div
+          className={twMerge(
+            'flex h-5/6 w-10/12	flex-col	overflow-hidden rounded-lg',
+            modalName === 'login' && 'h-auto w-[400px]',
+          )}
+        >
+          <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>
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/src/components/FunctionalArea/Modal/Modal.constants.ts b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.constants.ts
similarity index 100%
rename from src/components/FunctionalArea/Modal/Modal.constants.ts
rename to src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.constants.ts
diff --git a/src/components/FunctionalArea/Modal/ModalLayout/index.ts b/src/components/FunctionalArea/Modal/ModalLayout/index.ts
new file mode 100644
index 00000000..357e7d05
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/ModalLayout/index.ts
@@ -0,0 +1 @@
+export { ModalLayout } from './ModalLayout.component';
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModal.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModal.tsx
index 9ea913e3..0414f4a9 100644
--- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModal.tsx
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModal.tsx
@@ -1,16 +1,16 @@
-import Image from 'next/image';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { getPublications } from '@/redux/publications/publications.thunks';
 import { useEffect, useMemo } from 'react';
 import { publicationsListDataSelector } from '@/redux/publications/publications.selectors';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
-import spinnerIcon from '@/assets/vectors/icons/spinner.svg';
 import { modelsNameMapSelector } from '@/redux/models/models.selectors';
 import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
+import { LoadingIndicator } from '@/shared/LoadingIndicator';
 import {
   PublicationsTable,
   PublicationsTableData,
 } from './PublicationsTable/PublicationsTable.component';
+import { PublicationsModalLayout } from './PublicationsModalLayout';
 
 export const PublicationsModal = (): JSX.Element => {
   const dispatch = useAppDispatch();
@@ -37,19 +37,10 @@ export const PublicationsModal = (): JSX.Element => {
   }, [data, dispatch]);
 
   return (
-    <div className="flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-white">
-      {/* <PublicationsSearch /> */}
-      {data ? (
-        <PublicationsTable data={parsedData} />
-      ) : (
-        <Image
-          src={spinnerIcon}
-          alt="spinner icon"
-          height={40}
-          width={40}
-          className="animate-spin"
-        />
-      )}
-    </div>
+    <PublicationsModalLayout>
+      <div className="flex w-full flex-1 flex-col items-center justify-center overflow-hidden bg-white">
+        {data ? <PublicationsTable data={parsedData} /> : <LoadingIndicator />}
+      </div>
+    </PublicationsModalLayout>
   );
 };
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.component.tsx
new file mode 100644
index 00000000..8d92fe19
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.component.tsx
@@ -0,0 +1,45 @@
+import { twMerge } from 'tailwind-merge';
+import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { closeModal } from '@/redux/modal/modal.slice';
+import { Icon } from '@/shared/Icon';
+import { filteredSizeSelector } from '@/redux/publications/publications.selectors';
+import { MODAL_ROLE } from './PublicationsModalLayout.constants';
+import { PublicationsSearch } from '../PublicationsSearch';
+
+type ModalLayoutProps = {
+  children: React.ReactNode;
+};
+
+export const PublicationsModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
+  const dispatch = useAppDispatch();
+  const numberOfPublications = useAppSelector(filteredSizeSelector);
+
+  const handleCloseModal = (): void => {
+    dispatch(closeModal());
+  };
+
+  return (
+    <div
+      className={twMerge('absolute left-0 top-0 z-10 h-full w-full bg-cetacean-blue/[.48]')}
+      role={MODAL_ROLE}
+    >
+      <div className="flex h-full w-full items-center justify-center">
+        <div className={twMerge('flex h-5/6 w-10/12	flex-col	overflow-hidden rounded-lg')}>
+          <div className="flex items-center  justify-between bg-white p-[24px] text-xl">
+            <div className="font-semibold">
+              <div>Publications ({numberOfPublications} results)</div>
+            </div>
+            <div className="flex flex-row flex-nowrap items-center">
+              <PublicationsSearch />
+              <button type="button" onClick={handleCloseModal} aria-label="close button">
+                <Icon name="close" className="fill-font-500" />
+              </button>
+            </div>
+          </div>
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.constants.ts b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.constants.ts
new file mode 100644
index 00000000..b31cdcfb
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.constants.ts
@@ -0,0 +1 @@
+export const MODAL_ROLE = 'modal';
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/index.ts b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/index.ts
new file mode 100644
index 00000000..88be5bc7
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/index.ts
@@ -0,0 +1 @@
+export { PublicationsModalLayout } from './PublicationsModalLayout.component';
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsSearch/PublicationsSearch.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsSearch/PublicationsSearch.component.tsx
index a1cb12d8..be28bcdd 100644
--- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsSearch/PublicationsSearch.component.tsx
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsSearch/PublicationsSearch.component.tsx
@@ -1,28 +1,54 @@
-import { ChangeEvent, useEffect, useState } from 'react';
+import { ChangeEvent, useCallback, useEffect, useState } from 'react';
 import lensIcon from '@/assets/vectors/icons/lens.svg';
 import { useDebounce } from '@/hooks/useDebounce';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { getPublications } from '@/redux/publications/publications.thunks';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
-import { isLoadingSelector } from '@/redux/publications/publications.selectors';
+import {
+  isLoadingSelector,
+  selectedModelIdSelector,
+  sortColumnSelector,
+  sortOrderSelector,
+} from '@/redux/publications/publications.selectors';
 import Image from 'next/image';
+import { setPublicationSearchValue } from '@/redux/publications/publications.slice';
+import { DEFAULT_PAGE_SIZE } from '../PublicationsTable/PublicationsTable.constants';
 
 export const PublicationsSearch = (): JSX.Element => {
   const dispatch = useAppDispatch();
   const isLoading = useAppSelector(isLoadingSelector);
   const [value, setValue] = useState('');
   const debouncedValue = useDebounce<string>(value);
+  const sortColumn = useAppSelector(sortColumnSelector);
+  const sortOrder = useAppSelector(sortOrderSelector);
+  const selectedId = useAppSelector(selectedModelIdSelector);
 
   const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
     setValue(event.target.value);
   };
 
+  const handleSearch = useCallback((): void => {
+    dispatch(
+      getPublications({
+        params: {
+          page: 0,
+          length: DEFAULT_PAGE_SIZE,
+          sortColumn,
+          sortOrder,
+          search: debouncedValue,
+        },
+        modelId: selectedId,
+      }),
+    );
+  }, [debouncedValue, dispatch, selectedId, sortColumn, sortOrder]);
+
   useEffect(() => {
-    dispatch(getPublications({ params: { search: debouncedValue } }));
-  }, [dispatch, debouncedValue]);
+    dispatch(setPublicationSearchValue(debouncedValue));
+    handleSearch();
+  }, [debouncedValue, dispatch, handleSearch]);
 
   return (
-    <div className="mt-5">
+    <div className="relative mr-4">
       <input
         value={value}
         name="search-input"
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/FilterBySubmapHeader/FilterBySubmapHeader.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/FilterBySubmapHeader/FilterBySubmapHeader.component.tsx
index a09332e0..ab7b969d 100644
--- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/FilterBySubmapHeader/FilterBySubmapHeader.component.tsx
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/FilterBySubmapHeader/FilterBySubmapHeader.component.tsx
@@ -3,7 +3,11 @@ import { twMerge } from 'tailwind-merge';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { modelsIdsAndNamesSelector } from '@/redux/models/models.selectors';
-import { sortColumnSelector, sortOrderSelector } from '@/redux/publications/publications.selectors';
+import {
+  searchValueSelector,
+  sortColumnSelector,
+  sortOrderSelector,
+} from '@/redux/publications/publications.selectors';
 import { getPublications } from '@/redux/publications/publications.thunks';
 import { setSelectedModelId } from '@/redux/publications/publications.slice';
 import { Icon } from '@/shared/Icon';
@@ -14,6 +18,7 @@ export const FilterBySubmapHeader = (): JSX.Element => {
   const models = useAppSelector(modelsIdsAndNamesSelector);
   const sortColumn = useAppSelector(sortColumnSelector);
   const sortOrder = useAppSelector(sortOrderSelector);
+  const searchValue = useAppSelector(searchValueSelector);
 
   const handleChange = (modelId: number | undefined): void => {
     const newModelId = modelId ? String(modelId) : undefined;
@@ -27,8 +32,7 @@ export const FilterBySubmapHeader = (): JSX.Element => {
           length: DEFAULT_PAGE_SIZE,
           sortColumn,
           sortOrder,
-          // TODO
-          // search: get search from redux
+          search: searchValue,
         },
         modelId: newModelId,
       }),
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx
index e6e375e1..08cf5805 100644
--- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/PublicationsTable.component.tsx
@@ -8,6 +8,7 @@ import {
   sortColumnSelector,
   sortOrderSelector,
   selectedModelIdSelector,
+  searchValueSelector,
 } from '@/redux/publications/publications.selectors';
 import { getPublications } from '@/redux/publications/publications.thunks';
 import { Button } from '@/shared/Button';
@@ -83,14 +84,11 @@ export const PublicationsTable = ({ data }: PublicationsTableProps): JSX.Element
   const sortColumn = useAppSelector(sortColumnSelector);
   const sortOrder = useAppSelector(sortOrderSelector);
   const selectedId = useAppSelector(selectedModelIdSelector);
+  const searchValue = useAppSelector(searchValueSelector);
 
   const reduxPagination = useAppSelector(paginationSelector);
   const [pagination, setPagination] = useState(reduxPagination);
 
-  // useEffect(() => {
-  //   dispatch(getPublications({ page: pagination.pageIndex, length: DEFAULT_PAGE_SIZE }));
-  // }, [pagination, dispatch]);
-
   const onPaginationChange: OnChangeFn<PaginationState> = updater => {
     /** updating state this way is forced by table library */
     // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -104,8 +102,7 @@ export const PublicationsTable = ({ data }: PublicationsTableProps): JSX.Element
           length: DEFAULT_PAGE_SIZE,
           sortColumn,
           sortOrder,
-          // TODO
-          // search: get search from redux
+          search: searchValue,
         },
         modelId: selectedId,
       }),
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/SortByHeader/SortByHeader.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/SortByHeader/SortByHeader.component.tsx
index 943b8793..27c9aeac 100644
--- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/SortByHeader/SortByHeader.component.tsx
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsTable/SortByHeader/SortByHeader.component.tsx
@@ -3,7 +3,10 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { setSortOrderAndColumn } from '@/redux/publications/publications.slice';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { Icon } from '@/shared/Icon';
-import { sortColumnSelector } from '@/redux/publications/publications.selectors';
+import {
+  searchValueSelector,
+  sortColumnSelector,
+} from '@/redux/publications/publications.selectors';
 import { SortColumn, SortOrder } from '@/redux/publications/publications.types';
 import { getPublications } from '@/redux/publications/publications.thunks';
 import { DEFAULT_PAGE_SIZE } from '../PublicationsTable.constants';
@@ -16,6 +19,7 @@ type SortByHeaderProps = {
 export const SortByHeader = ({ columnName, children }: SortByHeaderProps): JSX.Element => {
   const activeColumn = useAppSelector(sortColumnSelector);
   const [sortDirection, setSortDirection] = useState<SortOrder | undefined>();
+  const searchValue = useAppSelector(searchValueSelector);
   const dispatch = useAppDispatch();
   // if columnName is the same as the current sortColumn, then sort in the opposite direction
 
@@ -36,8 +40,7 @@ export const SortByHeader = ({ columnName, children }: SortByHeaderProps): JSX.E
           length: DEFAULT_PAGE_SIZE,
           sortColumn: columnName,
           sortOrder: newSortDirection,
-          // TODO
-          // search: get search from redux
+          search: searchValue,
         },
       }),
     );
diff --git a/src/redux/publications/publications.mock.ts b/src/redux/publications/publications.mock.ts
index 7bf62fd7..3ae459cd 100644
--- a/src/redux/publications/publications.mock.ts
+++ b/src/redux/publications/publications.mock.ts
@@ -6,5 +6,6 @@ export const PUBLICATIONS_INITIAL_STATE_MOCK: PublicationsState = {
   error: { name: '', message: '' },
   sortColumn: '',
   sortOrder: 'asc',
+  searchValue: '',
   selectedModelId: undefined,
 };
diff --git a/src/redux/publications/publications.reducers.ts b/src/redux/publications/publications.reducers.ts
index ef1137a9..3ae1b8d4 100644
--- a/src/redux/publications/publications.reducers.ts
+++ b/src/redux/publications/publications.reducers.ts
@@ -32,3 +32,10 @@ export const setSelectedModelIdReducer = (
 ): void => {
   state.selectedModelId = action.payload;
 };
+
+export const setSearchValueReducer = (
+  state: PublicationsState,
+  action: PayloadAction<string>,
+): void => {
+  state.searchValue = action.payload;
+};
diff --git a/src/redux/publications/publications.selectors.ts b/src/redux/publications/publications.selectors.ts
index ea37240d..8ed16fac 100644
--- a/src/redux/publications/publications.selectors.ts
+++ b/src/redux/publications/publications.selectors.ts
@@ -46,3 +46,8 @@ export const selectedModelIdSelector = createSelector(
   publicationsSelector,
   publications => publications.selectedModelId,
 );
+
+export const searchValueSelector = createSelector(
+  publicationsSelector,
+  publications => publications.searchValue,
+);
diff --git a/src/redux/publications/publications.slice.ts b/src/redux/publications/publications.slice.ts
index cdeaf562..ea942b9d 100644
--- a/src/redux/publications/publications.slice.ts
+++ b/src/redux/publications/publications.slice.ts
@@ -4,6 +4,7 @@ import {
   getPublicationsReducer,
   setSortOrderAndColumnReducer,
   setSelectedModelIdReducer,
+  setSearchValueReducer,
 } from './publications.reducers';
 
 const initialState: PublicationsState = {
@@ -12,6 +13,7 @@ const initialState: PublicationsState = {
   error: { name: '', message: '' },
   sortColumn: '',
   sortOrder: 'asc',
+  searchValue: '',
 };
 
 const publicationsSlice = createSlice({
@@ -20,12 +22,14 @@ const publicationsSlice = createSlice({
   reducers: {
     setSortOrderAndColumn: setSortOrderAndColumnReducer,
     setSelectedModelId: setSelectedModelIdReducer,
+    setPublicationSearchValue: setSearchValueReducer,
   },
   extraReducers: builder => {
     getPublicationsReducer(builder);
   },
 });
 
-export const { setSortOrderAndColumn, setSelectedModelId } = publicationsSlice.actions;
+export const { setSortOrderAndColumn, setSelectedModelId, setPublicationSearchValue } =
+  publicationsSlice.actions;
 
 export default publicationsSlice.reducer;
diff --git a/src/redux/publications/publications.types.ts b/src/redux/publications/publications.types.ts
index 0678269c..2654b800 100644
--- a/src/redux/publications/publications.types.ts
+++ b/src/redux/publications/publications.types.ts
@@ -8,6 +8,7 @@ export type PublicationsState = FetchDataState<PublicationsResponse> & {
   sortColumn: SortColumn;
   sortOrder: SortOrder;
   selectedModelId?: string;
+  searchValue: string;
 };
 
 export type PublicationsQueryParams = {
diff --git a/src/shared/LoadingIndicator/LoadingIndicator.component.tsx b/src/shared/LoadingIndicator/LoadingIndicator.component.tsx
new file mode 100644
index 00000000..9f047575
--- /dev/null
+++ b/src/shared/LoadingIndicator/LoadingIndicator.component.tsx
@@ -0,0 +1,23 @@
+import Image from 'next/image';
+import spinnerIcon from '@/assets/vectors/icons/spinner.svg';
+
+type LoadingIndicatorProps = {
+  height?: number;
+  width?: number;
+};
+
+const DEFAULT_HEIGHT = 16;
+const DEFAULT_WIDTH = 16;
+
+export const LoadingIndicator = ({
+  height = DEFAULT_HEIGHT,
+  width = DEFAULT_WIDTH,
+}: LoadingIndicatorProps): JSX.Element => (
+  <Image
+    src={spinnerIcon}
+    alt="spinner icon"
+    height={height}
+    width={width}
+    className="animate-spin"
+  />
+);
diff --git a/src/shared/LoadingIndicator/index.ts b/src/shared/LoadingIndicator/index.ts
new file mode 100644
index 00000000..469606fa
--- /dev/null
+++ b/src/shared/LoadingIndicator/index.ts
@@ -0,0 +1 @@
+export { LoadingIndicator } from './LoadingIndicator.component';
-- 
GitLab