diff --git a/.eslintrc.json b/.eslintrc.json index c2c5d8bebf09edf9adce7bce3c9693893bd8f0ac..f7df93bc3522f3a2dd376c7b93534d03b083103a 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 75808add8c1e27212e36abb95c3728ddcbe4d78e..9a2e9c9b58ba1d0ee3fba92c5dae6cf880027025 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 e23f24129b82b6f95c5e8dd4d90d7317a842068f..8b38fda6785369dffa380a4849599412e8387f09 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 91a712ab78031d39bdc89008d1f8bbc7e67ed5b2..14af66353e1e7f94c1b79dbf41bf5f4b6dc424bd 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 e5a94463f193a32e5a7fd30958e8f7a45649fc7a..83a5c29958bc523d8becaf55be32cbcf3b2f3144 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 af462cab5db5d8323374e6309a741e8d28f80aae..20426d94766970d75e16b3c4b22c1dd89f7626e2 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 bab067763ad915e6595a45fc09b3ac61e4e55bad..47ee77aa25583fde85d11acdd9fe9ba64a0f3a8c 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 0000000000000000000000000000000000000000..3e3ce70f02bd81e9cd128cd52674a87a4e49589c --- /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 0000000000000000000000000000000000000000..fb360a4f122bc49d1841f9b42a9388804e027f7b --- /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 0000000000000000000000000000000000000000..5cc9f02bc9f61db3de8fca8484a0ae58f3522e2f --- /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 0000000000000000000000000000000000000000..a49b98fffb4585ab3afef535d62b364377f20a6b --- /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 0000000000000000000000000000000000000000..42e426de011a870792b92c137a4b4bda9f10029c --- /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 0000000000000000000000000000000000000000..b299eaa1cac1b9d466dcff6e804ff5cab9472f3e --- /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 0000000000000000000000000000000000000000..7976c8fc402abbf5a0c8f9774ff53bb26496df90 --- /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 d1335018a8dc60c21274dfa9f543fdedaad9ec72..c385726dc7809b109c0891d7fb930ff856de17a7 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 0000000000000000000000000000000000000000..2b61453e4b4469bf51cc26f440f5b49985c302b6 --- /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 0000000000000000000000000000000000000000..c62bc5d9438bc3742549ae737db262e5567ca1dc --- /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 4eca2c5bd0dd2690b27fe51b42b3d3b9f2e92394..ce605cba16e8602fe0eccd1f17c8c26bdcc685d1 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)'],