diff --git a/front/src/modules/auth/states/currentUserState.ts b/front/src/modules/auth/states/currentUserState.ts index e3f405e79..2361c402b 100644 --- a/front/src/modules/auth/states/currentUserState.ts +++ b/front/src/modules/auth/states/currentUserState.ts @@ -3,6 +3,6 @@ import { atom } from 'recoil'; import { User } from '@/users/interfaces/user.interface'; export const currentUserState = atom({ - key: 'auth/current-user', + key: 'currentUserState', default: null, }); diff --git a/front/src/modules/auth/states/isAuthenticatedState.ts b/front/src/modules/auth/states/isAuthenticatedState.ts index 96c8dbe9c..c29f2d3b1 100644 --- a/front/src/modules/auth/states/isAuthenticatedState.ts +++ b/front/src/modules/auth/states/isAuthenticatedState.ts @@ -3,7 +3,7 @@ import { selector } from 'recoil'; import { currentUserState } from './currentUserState'; export const isAuthenticatedState = selector({ - key: 'auth/is-authenticated', + key: 'isAuthenticatedState', get: ({ get }) => { const user = get(currentUserState); return !!user; diff --git a/front/src/modules/auth/states/isAuthenticatingState.ts b/front/src/modules/auth/states/isAuthenticatingState.ts index 3935f804f..49705fca3 100644 --- a/front/src/modules/auth/states/isAuthenticatingState.ts +++ b/front/src/modules/auth/states/isAuthenticatingState.ts @@ -1,6 +1,6 @@ import { atom } from 'recoil'; export const isAuthenticatingState = atom({ - key: 'auth/is-authenticating', + key: 'isAuthenticatingState', default: true, }); diff --git a/front/src/modules/ui/icons/components/IconSidebarLeftCollapse.tsx b/front/src/modules/ui/icons/components/IconSidebarLeftCollapse.tsx new file mode 100644 index 000000000..c18df9f99 --- /dev/null +++ b/front/src/modules/ui/icons/components/IconSidebarLeftCollapse.tsx @@ -0,0 +1 @@ +export { TbLayoutSidebarLeftCollapse as IconSidebarLeftCollapse } from 'react-icons/tb'; diff --git a/front/src/modules/ui/icons/components/IconSidebarRightCollapse.tsx b/front/src/modules/ui/icons/components/IconSidebarRightCollapse.tsx new file mode 100644 index 000000000..832485a7e --- /dev/null +++ b/front/src/modules/ui/icons/components/IconSidebarRightCollapse.tsx @@ -0,0 +1 @@ +export { TbLayoutSidebarRightCollapse as IconSidebarRightCollapse } from 'react-icons/tb'; diff --git a/front/src/modules/ui/icons/index.ts b/front/src/modules/ui/icons/index.ts index 129b40c24..3be39c9e3 100644 --- a/front/src/modules/ui/icons/index.ts +++ b/front/src/modules/ui/icons/index.ts @@ -1,3 +1,5 @@ export { IconAddressBook } from './components/IconAddressBook'; export { IconComment } from './components/IconComment'; +export { IconSidebarLeftCollapse } from './components/IconSidebarLeftCollapse'; +export { IconSidebarRightCollapse } from './components/IconSidebarRightCollapse'; export { IconAward } from '@tabler/icons-react'; diff --git a/front/src/modules/ui/layout/AppLayout.tsx b/front/src/modules/ui/layout/AppLayout.tsx index b74841989..eeffdbe06 100644 --- a/front/src/modules/ui/layout/AppLayout.tsx +++ b/front/src/modules/ui/layout/AppLayout.tsx @@ -1,10 +1,12 @@ import styled from '@emotion/styled'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { CommandMenu } from '@/search/components/CommandMenu'; import { Navbar } from './navbar/Navbar'; +import { isNavbarOpenedState } from './states/isNavbarOpenedState'; +import { MOBILE_VIEWPORT } from './styles/themes'; const StyledLayout = styled.div` display: flex; @@ -18,9 +20,17 @@ const StyledLayout = styled.div` const NAVBAR_WIDTH = '236px'; const MainContainer = styled.div` + overflow: hidden; display: flex; flex-direction: row; - width: calc(100% - ${NAVBAR_WIDTH}); + width: ${() => + useRecoilValue(isNavbarOpenedState) + ? `(calc(100% - ${NAVBAR_WIDTH})` + : '100%'}; + + @media (max-width: ${MOBILE_VIEWPORT}px) { + width: ${() => (useRecoilValue(isNavbarOpenedState) ? '0' : '100%')}; + } `; type OwnProps = { diff --git a/front/src/modules/ui/layout/navbar/NavItem.tsx b/front/src/modules/ui/layout/navbar/NavItem.tsx index 20b125c57..c9d6db51f 100644 --- a/front/src/modules/ui/layout/navbar/NavItem.tsx +++ b/front/src/modules/ui/layout/navbar/NavItem.tsx @@ -2,6 +2,8 @@ import { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from '@emotion/styled'; +import { MOBILE_VIEWPORT } from '../styles/themes'; + type OwnProps = { label: string; to: string; @@ -33,6 +35,10 @@ const StyledItem = styled.button` color: ${(props) => props.theme.text100}; } margin-bottom: calc(${(props) => props.theme.spacing(1)} / 2); + + @media (max-width: ${MOBILE_VIEWPORT}px) { + font-size: ${(props) => props.theme.fontSizeLarge}; + } `; const StyledItemLabel = styled.div` diff --git a/front/src/modules/ui/layout/navbar/Navbar.tsx b/front/src/modules/ui/layout/navbar/Navbar.tsx index a227d1f5c..490753f25 100644 --- a/front/src/modules/ui/layout/navbar/Navbar.tsx +++ b/front/src/modules/ui/layout/navbar/Navbar.tsx @@ -1,17 +1,31 @@ import { TbBuilding, TbUser } from 'react-icons/tb'; import { useMatch, useResolvedPath } from 'react-router-dom'; import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { isNavbarOpenedState } from '../states/isNavbarOpenedState'; +import { MOBILE_VIEWPORT } from '../styles/themes'; import NavItem from './NavItem'; import NavTitle from './NavTitle'; import WorkspaceContainer from './WorkspaceContainer'; +const NavbarContent = styled.div` + display: ${() => (useRecoilValue(isNavbarOpenedState) ? 'block' : 'none')}; +`; + const NavbarContainer = styled.div` - display: flex; flex-direction: column; - width: 220px; + width: ${() => (useRecoilValue(isNavbarOpenedState) ? '220px' : '0')}; padding: ${(props) => props.theme.spacing(2)}; flex-shrink: 0; + overflow: hidden; + + @media (max-width: ${MOBILE_VIEWPORT}px) { + width: ${(props) => + useRecoilValue(isNavbarOpenedState) + ? `calc(100% - ` + props.theme.spacing(4) + `)` + : '0'}; `; const NavItemsContainer = styled.div` @@ -24,32 +38,34 @@ export function Navbar() { return ( <> - - - - } - active={ - !!useMatch({ - path: useResolvedPath('/people').pathname, - end: true, - }) - } - /> - } - active={ - !!useMatch({ - path: useResolvedPath('/companies').pathname, - end: true, - }) - } - /> - + + + + + } + active={ + !!useMatch({ + path: useResolvedPath('/people').pathname, + end: true, + }) + } + /> + } + active={ + !!useMatch({ + path: useResolvedPath('/companies').pathname, + end: true, + }) + } + /> + + ); diff --git a/front/src/modules/ui/layout/navbar/WorkspaceContainer.tsx b/front/src/modules/ui/layout/navbar/WorkspaceContainer.tsx index 7930bef89..a663e8d36 100644 --- a/front/src/modules/ui/layout/navbar/WorkspaceContainer.tsx +++ b/front/src/modules/ui/layout/navbar/WorkspaceContainer.tsx @@ -1,19 +1,30 @@ import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; +import { IconSidebarLeftCollapse } from '@/ui/icons'; -const StyledContainer = styled.button` - display: inline-flex; +import { isNavbarOpenedState } from '../states/isNavbarOpenedState'; + +const StyledContainer = styled.div` + display: flex; + justify-content: space-between; height: 34px; align-items: center; - cursor: pointer; user-select: none; border: 0; background: inherit; padding: ${(props) => props.theme.spacing(2)}; + padding-top: ${(props) => props.theme.spacing(1)}; margin-left: ${(props) => props.theme.spacing(1)}; align-self: flex-start; + width: 100%; +`; + +const LogoAndNameContainer = styled.div` + display: flex; + align-items: center; + cursor: pointer; `; type StyledLogoProps = { @@ -36,8 +47,27 @@ const StyledName = styled.div` color: ${(props) => props.theme.text80}; `; +const CollapseButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + + width: 32px; + height: 32px; + + user-select: none; + border: 0; + background: inherit; + + padding: 0; + cursor: pointer; + + color: ${(props) => props.theme.text30}; +`; + function WorkspaceContainer() { const currentUser = useRecoilValue(currentUserState); + const [isNavOpen, setIsNavOpen] = useRecoilState(isNavbarOpenedState); const currentWorkspace = currentUser?.workspaceMember?.workspace; @@ -47,8 +77,15 @@ function WorkspaceContainer() { return ( - - {currentWorkspace?.displayName} + + + {currentWorkspace?.displayName} + + {isNavOpen && ( + setIsNavOpen(!isNavOpen)}> + + + )} ); } diff --git a/front/src/modules/ui/layout/states/isNavbarOpenedState.ts b/front/src/modules/ui/layout/states/isNavbarOpenedState.ts new file mode 100644 index 000000000..ed211773c --- /dev/null +++ b/front/src/modules/ui/layout/states/isNavbarOpenedState.ts @@ -0,0 +1,10 @@ +import { atom } from 'recoil'; + +import { MOBILE_VIEWPORT } from '../styles/themes'; + +const isMobile = window.innerWidth <= MOBILE_VIEWPORT; + +export const isNavbarOpenedState = atom({ + key: 'ui/isNavbarOpenedState', + default: !isMobile, +}); diff --git a/front/src/modules/ui/layout/states/isThemeEnabledState.ts b/front/src/modules/ui/layout/states/isThemeEnabledState.ts new file mode 100644 index 000000000..2337edf2b --- /dev/null +++ b/front/src/modules/ui/layout/states/isThemeEnabledState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isThemeEnabledState = atom({ + key: 'isThemeEnabledState', + default: true, +}); diff --git a/front/src/modules/ui/layout/states/themeEnabledState.ts b/front/src/modules/ui/layout/states/themeEnabledState.ts deleted file mode 100644 index 5a819aa13..000000000 --- a/front/src/modules/ui/layout/states/themeEnabledState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from 'recoil'; - -export const themeEnabledState = atom({ - key: 'ui/theme-enabled', - default: true, -}); diff --git a/front/src/modules/ui/layout/styles/themes.ts b/front/src/modules/ui/layout/styles/themes.ts index 8cd9b08bf..30047daac 100644 --- a/front/src/modules/ui/layout/styles/themes.ts +++ b/front/src/modules/ui/layout/styles/themes.ts @@ -139,4 +139,6 @@ export const textInputStyle = (props: any) => export const lightTheme = { ...commonTheme, ...lightThemeSpecific }; export const darkTheme = { ...commonTheme, ...darkThemeSpecific }; +export const MOBILE_VIEWPORT = 768; + export type ThemeType = typeof lightTheme; diff --git a/front/src/modules/ui/layout/top-bar/TopBar.tsx b/front/src/modules/ui/layout/top-bar/TopBar.tsx index 0cbe8146a..abf00301f 100644 --- a/front/src/modules/ui/layout/top-bar/TopBar.tsx +++ b/front/src/modules/ui/layout/top-bar/TopBar.tsx @@ -1,6 +1,11 @@ import { ReactNode } from 'react'; import { TbPlus } from 'react-icons/tb'; import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; + +import { IconSidebarRightCollapse } from '@/ui/icons'; + +import { isNavbarOpenedState } from '../states/isNavbarOpenedState'; export const TOP_BAR_MIN_HEIGHT = '40px'; @@ -39,6 +44,24 @@ const AddButtonContainer = styled.div` margin-right: ${(props) => props.theme.spacing(1)}; `; +const CollapseButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + + width: 32px; + height: 32px; + + user-select: none; + border: 0; + background: inherit; + + padding: 0; + cursor: pointer; + + color: ${(props) => props.theme.text30}; +`; + type OwnProps = { title: string; icon: ReactNode; @@ -46,9 +69,16 @@ type OwnProps = { }; export function TopBar({ title, icon, onAddButtonClick }: OwnProps) { + const [isNavOpen, setIsNavOpen] = useRecoilState(isNavbarOpenedState); + return ( <> + {!isNavOpen && ( + setIsNavOpen(!isNavOpen)}> + + + )} {icon} {title} {onAddButtonClick && ( diff --git a/front/src/modules/ui/tables/states/isSomeInputInEditModeState.ts b/front/src/modules/ui/tables/states/isSomeInputInEditModeState.ts index 6affc85cb..71fdb0172 100644 --- a/front/src/modules/ui/tables/states/isSomeInputInEditModeState.ts +++ b/front/src/modules/ui/tables/states/isSomeInputInEditModeState.ts @@ -1,6 +1,6 @@ import { atom } from 'recoil'; export const isSomeInputInEditModeState = atom({ - key: 'ui/table/is-in-edit-mode', + key: 'isSomeInputInEditModeState', default: false, }); diff --git a/front/src/modules/ui/tables/states/rowSelectionState.ts b/front/src/modules/ui/tables/states/rowSelectionState.ts index 101bc9c12..acf8943f6 100644 --- a/front/src/modules/ui/tables/states/rowSelectionState.ts +++ b/front/src/modules/ui/tables/states/rowSelectionState.ts @@ -2,6 +2,6 @@ import { RowSelectionState } from '@tanstack/react-table'; import { atom } from 'recoil'; export const currentRowSelectionState = atom({ - key: 'ui/table-row-selection-state', + key: 'currentRowSelectionState', default: {}, }); diff --git a/front/src/modules/ui/tables/states/selectedRowIdsState.ts b/front/src/modules/ui/tables/states/selectedRowIdsState.ts index 9e9784cb4..ce69c8ef8 100644 --- a/front/src/modules/ui/tables/states/selectedRowIdsState.ts +++ b/front/src/modules/ui/tables/states/selectedRowIdsState.ts @@ -3,7 +3,7 @@ import { selector } from 'recoil'; import { currentRowSelectionState } from './rowSelectionState'; export const selectedRowIdsState = selector({ - key: 'ui/table-selected-row-ids', + key: 'selectedRowIdsState', get: ({ get }) => { const currentRowSelection = get(currentRowSelectionState);