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