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