diff --git a/src/pages/commonFeed/CommonFeedPage.tsx b/src/pages/commonFeed/CommonFeedPage.tsx index ab1f006173..560724d47a 100644 --- a/src/pages/commonFeed/CommonFeedPage.tsx +++ b/src/pages/commonFeed/CommonFeedPage.tsx @@ -1,10 +1,13 @@ -import React, { FC, useEffect } from "react"; -import { useDispatch } from "react-redux"; +import React, { FC, useEffect, useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; import { InboxItemType } from "@/shared/constants"; import { MainRoutesProvider } from "@/shared/contexts"; import { MultipleSpacesLayoutPageContent } from "@/shared/layouts"; -import { multipleSpacesLayoutActions } from "@/store/states"; +import { + multipleSpacesLayoutActions, + selectMultipleSpacesLayoutMainWidth, +} from "@/store/states"; import BaseCommonFeedPage, { CommonFeedPageRouterParams, } from "./BaseCommonFeedPage"; @@ -14,8 +17,8 @@ import { FeedLayoutSettings, HeaderContent, } from "./components"; -import { MIN_CHAT_WIDTH } from "./constants"; import { useActiveItemDataChange } from "./hooks"; +import { generateSplitViewMaxSizeGetter } from "./utils"; import styles from "./CommonFeedPage.module.scss"; export const FEED_LAYOUT_OUTER_STYLES: FeedLayoutOuterStyles = { @@ -23,11 +26,9 @@ export const FEED_LAYOUT_OUTER_STYLES: FeedLayoutOuterStyles = { desktopChat: styles.desktopChat, }; -export const FEED_LAYOUT_SETTINGS: FeedLayoutSettings = { +export const BASE_FEED_LAYOUT_SETTINGS: FeedLayoutSettings = { withDesktopChatTitle: false, sidenavWidth: 0, - getSplitViewMaxSize: (width) => - width < 1100 ? MIN_CHAT_WIDTH : Math.floor(width * 0.6), }; const renderContentWrapper: RenderCommonFeedContentWrapper = ({ @@ -56,7 +57,15 @@ const renderContentWrapper: RenderCommonFeedContentWrapper = ({ const CommonFeedPage: FC = () => { const { id: commonId } = useParams(); const dispatch = useDispatch(); + const layoutMainWidth = useSelector(selectMultipleSpacesLayoutMainWidth); const onActiveItemDataChange = useActiveItemDataChange(); + const feedLayoutSettings = useMemo( + () => ({ + ...BASE_FEED_LAYOUT_SETTINGS, + getSplitViewMaxSize: generateSplitViewMaxSizeGetter(layoutMainWidth), + }), + [layoutMainWidth], + ); useEffect(() => { dispatch( @@ -79,7 +88,7 @@ const CommonFeedPage: FC = () => { renderContentWrapper={renderContentWrapper} onActiveItemDataChange={onActiveItemDataChange} feedLayoutOuterStyles={FEED_LAYOUT_OUTER_STYLES} - feedLayoutSettings={FEED_LAYOUT_SETTINGS} + feedLayoutSettings={feedLayoutSettings} /> ); diff --git a/src/pages/commonFeed/utils/generateSplitViewMaxSizeGetter.ts b/src/pages/commonFeed/utils/generateSplitViewMaxSizeGetter.ts new file mode 100644 index 0000000000..79d28c2989 --- /dev/null +++ b/src/pages/commonFeed/utils/generateSplitViewMaxSizeGetter.ts @@ -0,0 +1,6 @@ +import { MIN_CHAT_WIDTH } from "../constants"; + +export const generateSplitViewMaxSizeGetter = + (containerWidth: number): (() => number) => + () => + containerWidth < 1100 ? MIN_CHAT_WIDTH : Math.floor(containerWidth * 0.6); diff --git a/src/pages/commonFeed/utils/index.ts b/src/pages/commonFeed/utils/index.ts index eab2c89a6b..7e1db5d415 100644 --- a/src/pages/commonFeed/utils/index.ts +++ b/src/pages/commonFeed/utils/index.ts @@ -1 +1,2 @@ +export * from "./generateSplitViewMaxSizeGetter"; export * from "./getLastMessage"; diff --git a/src/pages/inbox/Inbox.tsx b/src/pages/inbox/Inbox.tsx index 1276e14008..c339bc9bb2 100644 --- a/src/pages/inbox/Inbox.tsx +++ b/src/pages/inbox/Inbox.tsx @@ -1,13 +1,16 @@ -import React, { CSSProperties, FC, ReactNode } from "react"; +import React, { CSSProperties, FC, ReactNode, useMemo } from "react"; import { useSelector } from "react-redux"; import { selectUserStreamsWithNotificationsAmount } from "@/pages/Auth/store/selectors"; -import { useActiveItemDataChange } from "@/pages/commonFeed/hooks"; import { MainRoutesProvider } from "@/shared/contexts"; import { MultipleSpacesLayoutPageContent } from "@/shared/layouts"; +import { selectMultipleSpacesLayoutMainWidth } from "@/store/states"; +import { FeedLayoutSettings } from "../commonFeed"; import { FEED_LAYOUT_OUTER_STYLES, - FEED_LAYOUT_SETTINGS, + BASE_FEED_LAYOUT_SETTINGS, } from "../commonFeed/CommonFeedPage"; +import { useActiveItemDataChange } from "../commonFeed/hooks"; +import { generateSplitViewMaxSizeGetter } from "../commonFeed/utils"; import BaseInboxPage from "./BaseInbox"; import { HeaderContent } from "./components"; @@ -15,7 +18,15 @@ const InboxPage: FC = () => { const userStreamsWithNotificationsAmount = useSelector( selectUserStreamsWithNotificationsAmount(), ); + const layoutMainWidth = useSelector(selectMultipleSpacesLayoutMainWidth); const onActiveItemDataChange = useActiveItemDataChange(); + const feedLayoutSettings = useMemo( + () => ({ + ...BASE_FEED_LAYOUT_SETTINGS, + getSplitViewMaxSize: generateSplitViewMaxSizeGetter(layoutMainWidth), + }), + [layoutMainWidth], + ); const renderContentWrapper = ( children: ReactNode, @@ -41,7 +52,7 @@ const InboxPage: FC = () => { renderContentWrapper={renderContentWrapper} onActiveItemDataChange={onActiveItemDataChange} feedLayoutOuterStyles={FEED_LAYOUT_OUTER_STYLES} - feedLayoutSettings={FEED_LAYOUT_SETTINGS} + feedLayoutSettings={feedLayoutSettings} /> ); diff --git a/src/shared/constants/storageKey.ts b/src/shared/constants/storageKey.ts index 5012104b30..5713c55929 100644 --- a/src/shared/constants/storageKey.ts +++ b/src/shared/constants/storageKey.ts @@ -1,4 +1,5 @@ export enum StorageKey { ChatSize = "chatSize", Theme = "theme", + MultipleSpacesLayoutSidenavState = "msl-sidenav-state", } diff --git a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss index 7446d06d3b..64b3e94956 100644 --- a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss +++ b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss @@ -3,9 +3,9 @@ .container { --main-mw: calc(120rem + var(--sb-h-indent, 0)); - --main-pl: calc(var(--sb-width) + var(--sb-h-indent, 0)); + --main-pl: calc(var(--sb-h-indent, 0)); --sb-max-width: unset; - --sb-width: 0rem; + --sb-width: 21rem; --sb-content-max-width: 100%; --sb-content-width: 100%; --sb-content-pb: 0; @@ -16,6 +16,7 @@ --scroll-bg-color: #{$c-shades-white}; --scroll-thumb-color: #{$c-neutrals-200}; --layout-tabs-height: 0rem; + --header-h: 3.5rem; min-height: 100vh; height: 100%; @@ -31,12 +32,35 @@ --sb-width: 100%; --sb-content-width: 100%; --layout-tabs-height: 4rem; + --header-h: 0; } } +.containerWithOpenedSidenav { + --main-pl: calc(var(--sb-width) + var(--sb-h-indent, 0)); -.main { - --header-h: 3.5rem; + @include tablet { + --main-pl: unset; + } +} +.sidenav { + top: var(--header-h); +} + +.sidenavContentWrapper { + border-right: 0.25rem solid $c-light-gray-2; + + @include tablet { + border: 0; + } +} + +.sidenavContent { + margin: 0 auto; + max-width: 25rem; +} + +.main { flex: 1; max-width: var(--main-mw); padding-left: var(--main-pl); diff --git a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.tsx b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.tsx index a286c706db..b5596db174 100644 --- a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.tsx @@ -1,39 +1,124 @@ -import React, { CSSProperties, FC } from "react"; -import { useSelector } from "react-redux"; +import React, { CSSProperties, FC, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { useWindowSize } from "react-use"; +import classNames from "classnames"; import { useLayoutRouteContext, MultipleSpacesLayoutRouteOptions, } from "@/pages/App/router"; +import { StorageKey } from "@/shared/constants"; import { MainRoutesProvider } from "@/shared/contexts"; +import { useLockedBody, useQueryParams } from "@/shared/hooks"; import { useIsTabletView } from "@/shared/hooks/viewport"; -import { selectMultipleSpacesLayoutBackUrl } from "@/store/states"; +import { Sidenav } from "@/shared/ui-kit"; +import { checkIsSidenavOpen, closeSidenav, openSidenav } from "@/shared/utils"; +import { + multipleSpacesLayoutActions, + selectMultipleSpacesLayoutBackUrl, +} from "@/store/states"; import { getSidenavLeft } from "../CommonSidenavLayout/utils"; -import { Header, Menu } from "./components"; +import { Header, SidenavContent } from "./components"; import styles from "./MultipleSpacesLayout.module.scss"; +const MULTIPLE_SPACES_LAYOUT_SIDENAV_OPEN_STATE = "open"; +const MULTIPLE_SPACES_LAYOUT_SIDENAV_WIDTH = 336; + const MultipleSpacesLayout: FC = (props) => { const { children } = props; + const queryParams = useQueryParams(); + const dispatch = useDispatch(); const { routeOptions = {} } = useLayoutRouteContext(); const backUrl = useSelector(selectMultipleSpacesLayoutBackUrl); const isTabletView = useIsTabletView(); const { width } = useWindowSize(); + const { lockBodyScroll, unlockBodyScroll } = useLockedBody(); + const isSidenavOpenFromQueryParams = checkIsSidenavOpen(queryParams); + const [isSidenavOpen, setIsSidenavOpen] = useState( + () => + localStorage.getItem(StorageKey.MultipleSpacesLayoutSidenavState) === + MULTIPLE_SPACES_LAYOUT_SIDENAV_OPEN_STATE ?? + isSidenavOpenFromQueryParams, + ); const sidenavLeft = getSidenavLeft(width); const style = { "--sb-h-indent": `${sidenavLeft}px`, } as CSSProperties; + const mainWidth = + isSidenavOpen && !isTabletView + ? width - MULTIPLE_SPACES_LAYOUT_SIDENAV_WIDTH + : width; + + const handleSidenavOpen = () => { + setIsSidenavOpen(true); + localStorage.setItem( + StorageKey.MultipleSpacesLayoutSidenavState, + MULTIPLE_SPACES_LAYOUT_SIDENAV_OPEN_STATE, + ); + openSidenav(); + }; + + const handleSidenavClose = () => { + setIsSidenavOpen(false); + localStorage.removeItem(StorageKey.MultipleSpacesLayoutSidenavState); + closeSidenav(); + }; + + useEffect(() => { + dispatch(multipleSpacesLayoutActions.setMainWidth(mainWidth)); + }, [mainWidth]); + + useEffect(() => { + if (!isTabletView) { + return; + } + if (isSidenavOpenFromQueryParams) { + handleSidenavOpen(); + } else { + handleSidenavClose(); + } + }, [isSidenavOpenFromQueryParams, isTabletView]); + + useEffect(() => { + if (!isTabletView) { + return; + } + if (isSidenavOpen) { + lockBodyScroll(); + } else { + unlockBodyScroll(); + } + }, [isSidenavOpen, isTabletView]); return ( -
- +
+ + +
{!isTabletView && (
)} {children} diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/Header.module.scss b/src/shared/layouts/MultipleSpacesLayout/components/Header/Header.module.scss index e70e28c92a..73857ed126 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/Header.module.scss +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/Header.module.scss @@ -18,6 +18,12 @@ .leftContent { flex: 1; + display: flex; + align-items: center; +} + +.menuButton { + color: $c-gray-40; } .backLink { diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/Header.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/Header.tsx index e84ca15697..9269db76da 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/Header.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/Header.tsx @@ -3,6 +3,7 @@ import { useSelector } from "react-redux"; import { NavLink } from "react-router-dom"; import { authentificated, selectUser } from "@/pages/Auth/store/selectors"; import { LongLeftArrowIcon } from "@/shared/icons"; +import { TopNavigationOpenSidenavButton } from "@/shared/ui-kit"; import { getUserName } from "@/shared/utils"; import { ContentStyles, @@ -17,6 +18,8 @@ interface HeaderProps { backUrl?: string | null; withBreadcrumbs?: boolean; breadcrumbsItemsWithMenus?: boolean; + withMenuButton?: boolean; + onMenuClick?: () => void; } const Header: FC = (props) => { @@ -24,6 +27,8 @@ const Header: FC = (props) => { backUrl = null, withBreadcrumbs = true, breadcrumbsItemsWithMenus = true, + withMenuButton = true, + onMenuClick, } = props; const isAuthenticated = useSelector(authentificated()); const user = useSelector(selectUser()); @@ -40,6 +45,12 @@ const Header: FC = (props) => { return (
+ {withMenuButton && ( + + )} {withBreadcrumbs && !backUrl && ( )} diff --git a/src/shared/layouts/MultipleSpacesLayout/components/PageContent/PageContent.module.scss b/src/shared/layouts/MultipleSpacesLayout/components/PageContent/PageContent.module.scss index 895649a467..23768310a1 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/PageContent/PageContent.module.scss +++ b/src/shared/layouts/MultipleSpacesLayout/components/PageContent/PageContent.module.scss @@ -30,7 +30,7 @@ position: fixed; top: var(--header-h); right: var(--sb-h-indent); - left: var(--sb-h-indent); + left: var(--main-pl); z-index: 2; height: var(--page-content-header); background-color: $c-shades-white; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/SidenavContent.module.scss b/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/SidenavContent.module.scss new file mode 100644 index 0000000000..20ecdbc718 --- /dev/null +++ b/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/SidenavContent.module.scss @@ -0,0 +1,28 @@ +@import "../../../../../constants"; + +.container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.closeIconWrapper { + margin-left: auto; + padding: 1.25rem 1.5rem; +} + +.noCommonsInfoContainer { + padding: 0 1rem; +} + +.noCommonsText { + margin: 0 0 1rem; + padding: 0; + text-align: center; +} + +.createCommonButton { + margin: 0 auto; +} diff --git a/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/SidenavContent.tsx b/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/SidenavContent.tsx new file mode 100644 index 0000000000..a63304bff0 --- /dev/null +++ b/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/SidenavContent.tsx @@ -0,0 +1,49 @@ +import React, { FC, ReactNode, useCallback, useRef } from "react"; +import classNames from "classnames"; +import { ButtonIcon } from "@/shared/components"; +import { Close2Icon } from "@/shared/icons"; +import { Button, ButtonVariant } from "@/shared/ui-kit"; +import { + Projects, + ProjectsRef, +} from "../../../CommonSidenavLayout/components/SidenavContent/components"; +import styles from "./SidenavContent.module.scss"; + +interface SidenavContentProps { + className?: string; + onClose?: () => void; +} + +const SidenavContent: FC = (props) => { + const { className, onClose } = props; + const projectsRef = useRef(null); + + const renderNoItemsInfo = useCallback((): ReactNode => { + return ( +
+

+ You do not have spaces yet. You might want to create a new common or + ask your friends for the link to an existing one. +

+ +
+ ); + }, []); + + return ( +
+ + + + +
+ ); +}; + +export default SidenavContent; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/index.ts b/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/index.ts new file mode 100644 index 0000000000..8f08b6bb75 --- /dev/null +++ b/src/shared/layouts/MultipleSpacesLayout/components/SidenavContent/index.ts @@ -0,0 +1 @@ +export { default as SidenavContent } from "./SidenavContent"; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/index.ts b/src/shared/layouts/MultipleSpacesLayout/components/index.ts index a5c2693dea..aacf5545b2 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/index.ts +++ b/src/shared/layouts/MultipleSpacesLayout/components/index.ts @@ -2,3 +2,4 @@ export * from "./Header"; export * from "./Menu"; export * from "./MenuPopUp"; export * from "./PageContent"; +export * from "./SidenavContent"; diff --git a/src/shared/ui-kit/Sidenav/Sidenav.module.scss b/src/shared/ui-kit/Sidenav/Sidenav.module.scss index cfe45ba443..fbe0603aa8 100644 --- a/src/shared/ui-kit/Sidenav/Sidenav.module.scss +++ b/src/shared/ui-kit/Sidenav/Sidenav.module.scss @@ -12,26 +12,26 @@ @include tablet { right: 0; - display: flex; - will-change: transform; - transform: translateX(-110vw); - transition: transform var(--sb-transition-duration) var(--sb-easing), - visibility 0s linear var(--sb-transition-duration); - overflow: hidden; - overscroll-behavior: contain; - visibility: hidden; } @media (prefers-reduced-motion: reduce) { --sb-transition-duration: 1ms; } } -.sidenavOpen { - @include tablet { - transform: translateX(0); - transition: transform var(--sb-transition-duration) var(--sb-easing); - visibility: visible; - } +.sidenavWithAnimation { + display: flex; + will-change: transform; + transform: translateX(-110vw); + transition: transform var(--sb-transition-duration) var(--sb-easing), + visibility 0s linear var(--sb-transition-duration); + overflow: hidden; + overscroll-behavior: contain; + visibility: hidden; +} +.sidenavWithAnimationOpen { + transform: translateX(0); + transition: transform var(--sb-transition-duration) var(--sb-easing); + visibility: visible; } .contentWrapper { diff --git a/src/shared/ui-kit/Sidenav/Sidenav.tsx b/src/shared/ui-kit/Sidenav/Sidenav.tsx index a5c67ef8ce..827f0a9962 100644 --- a/src/shared/ui-kit/Sidenav/Sidenav.tsx +++ b/src/shared/ui-kit/Sidenav/Sidenav.tsx @@ -8,19 +8,33 @@ import { checkIsSidenavOpen, closeSidenav } from "@/shared/utils"; import styles from "./Sidenav.module.scss"; interface SidenavProps { + className?: string; contentWrapperClassName?: string; style?: CSSProperties; + isOpen?: boolean; + shouldCheckViewportForOpenState?: boolean; + withAnimation?: boolean; onOpenToggle?: (isOpen: boolean) => void; } const Sidenav: FC = (props) => { - const { contentWrapperClassName, style, onOpenToggle, children } = props; + const { + className, + contentWrapperClassName, + style, + shouldCheckViewportForOpenState = true, + withAnimation, + onOpenToggle, + children, + } = props; const queryParams = useQueryParams(); const viewportStates = useAllViews(); - const isSidenavVisible = - !viewportStates.isTabletView || checkIsSidenavOpen(queryParams); - // Sidenav can be open only on tablet and lower viewports - const isSidenavOpen = viewportStates.isTabletView && isSidenavVisible; + // Sidenav can be open only on tablet and lower viewports if shouldCheckViewportForOpenState is `true` + const isAllowedToBeShown = + !shouldCheckViewportForOpenState || viewportStates.isTabletView; + const isSidenavOpen = + props.isOpen ?? (isAllowedToBeShown && checkIsSidenavOpen(queryParams)); + const isSidenavWithAnimation = withAnimation ?? viewportStates.isTabletView; const onSidebarKeyUp = (event: KeyboardEvent): void => { if (event.key === KeyboardKeys.Escape) { @@ -37,9 +51,15 @@ const Sidenav: FC = (props) => { return (