💄(frontend) add left panel

In the new interface there is a new left panel. We implement it and add it
to the MainLayout
This commit is contained in:
Nathan Panchout
2024-11-13 15:10:02 +01:00
committed by Anthony LC
parent e83c404e21
commit 8d514bd571
20 changed files with 203 additions and 70 deletions

View File

@@ -13,6 +13,7 @@ and this project adheres to
- 🔧(backend) add option to configure list of essential OIDC claims #525 & #531
- 🔧(helm) add option to disable default tls setting by @dominikkaminski #519
- 💄(frontend) Add left panel #420
## Changed

View File

@@ -1,6 +1,14 @@
import { Text, TextType } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
type IconProps = {
iconName: string;
className?: string;
};
export const Icon = ({ iconName, className }: IconProps) => {
return <span className={`material-icons ${className}`}>{iconName}</span>;
};
interface IconBGProps extends TextType {
iconName: string;
}

View File

@@ -6,5 +6,6 @@ export * from './Icon';
export * from './InfiniteScroll';
export * from './Link';
export * from './SideModal';
export * from './separators/SeparatedSection';
export * from './Text';
export * from './TextErrors';

View File

@@ -0,0 +1,32 @@
import { PropsWithChildren } from 'react';
import { css } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
import { Box } from '../Box';
type Props = {
showSeparator?: boolean;
};
export const SeparatedSection = ({
showSeparator = true,
children,
}: PropsWithChildren<Props>) => {
const theme = useCunninghamTheme();
const colors = theme.colorsTokens();
const spacings = theme.spacingsTokens();
return (
<Box
$css={css`
padding: ${spacings['base']} 0;
${showSeparator &&
css`
border-bottom: 1px solid ${colors?.['greyscale-200']};
`}
`}
>
{children}
</Box>
);
};

View File

@@ -5,6 +5,7 @@ import { tokens } from './cunningham-tokens';
type Tokens = typeof tokens.themes.default & Partial<typeof tokens.themes.dsfr>;
type ColorsTokens = Tokens['theme']['colors'];
type SpacingsTokens = Tokens['theme']['spacings'];
type ComponentTokens = Tokens['components'];
export type Theme = keyof typeof tokens.themes;
@@ -13,6 +14,7 @@ interface AuthStore {
setTheme: (theme: Theme) => void;
themeTokens: () => Partial<Tokens['theme']>;
colorsTokens: () => Partial<ColorsTokens>;
spacingsTokens: () => Partial<SpacingsTokens>;
componentTokens: () => ComponentTokens;
}
@@ -28,6 +30,7 @@ export const useCunninghamTheme = create<AuthStore>((set, get) => {
themeTokens: () => currentTheme().theme,
colorsTokens: () => currentTheme().theme.colors,
componentTokens: () => currentTheme().components,
spacingsTokens: () => currentTheme().theme.spacings,
setTheme: (theme: Theme) => {
set({ theme });
},

View File

@@ -43,7 +43,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
<>
<DocHeader doc={doc} />
{!doc.abilities.partial_update && (
<Box $margin={{ all: 'small', top: 'none' }}>
<Box $width="100%" $margin={{ all: 'small', top: 'none' }}>
<Alert type={VariantType.WARNING}>
{t(`Read only, you cannot edit this document.`)}
</Alert>
@@ -58,10 +58,10 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
)}
<Box
$background={colorsTokens()['primary-bg']}
$height="100%"
$direction="row"
$width="100%"
$margin={{ all: isMobile ? 'tiny' : 'small', top: 'none' }}
$css="overflow-x: clip;"
$css="overflow-x: clip; flex: 1;"
$position="relative"
>
<Card

View File

@@ -26,6 +26,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
return (
<>
<Card
$width="100%"
$margin={isMobile ? 'tiny' : 'small'}
aria-label={t('It is the card information about the document.')}
>

View File

@@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, BoxButton, Text } from '@/components';
import { useEditorStore, useHeadingStore } from '@/features/docs/doc-editor';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { useResponsiveStore } from '@/stores';
import { Heading } from './Heading';
@@ -43,7 +44,7 @@ export const TableContent = () => {
}
};
window.addEventListener('scroll', () => {
document.getElementById(MAIN_LAYOUT_ID)?.addEventListener('scroll', () => {
setTimeout(() => {
handleScroll();
}, 300);

View File

@@ -1,40 +0,0 @@
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
import { useCreateDoc, useTrans } from '@/features/docs/doc-management/';
import { useResponsiveStore } from '@/stores';
import { DocsGrid } from './DocsGrid';
export const DocsGridContainer = () => {
const { t } = useTranslation();
const { untitledDocument } = useTrans();
const { push } = useRouter();
const { isMobile } = useResponsiveStore();
const { mutate: createDoc } = useCreateDoc({
onSuccess: (doc) => {
void push(`/docs/${doc.id}`);
},
});
const handleCreateDoc = () => {
createDoc({ title: untitledDocument });
};
return (
<Box $overflow="auto">
<Box
$align="flex-end"
$justify="center"
$margin={isMobile ? 'small' : 'big'}
>
<Button onClick={handleCreateDoc}>{t('Create a new document')}</Button>
</Box>
<DocsGrid />
</Box>
);
};

View File

@@ -1 +0,0 @@
export * from './DocsGridContainer';

View File

@@ -1,5 +1,4 @@
import Image from 'next/image';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, StyledLink } from '@/components/';
@@ -24,7 +23,7 @@ export const Header = () => {
$width="100%"
$zIndex="100"
$padding={{ vertical: 'xtiny' }}
$css="box-shadow: 0 1px 4px #00000040;"
$css="border-bottom: 1px solid #EDEDED;"
>
<Box
$margin={{

View File

@@ -0,0 +1 @@
export const HEADER_HEIGHT = 52;

View File

@@ -0,0 +1,70 @@
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import { PropsWithChildren } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Icon, SeparatedSection } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useCreateDoc } from '@/features/docs';
import { HEADER_HEIGHT } from '@/features/header/conf';
import { useResponsiveStore } from '@/stores';
export const LeftPanel = ({ children }: PropsWithChildren) => {
const { t } = useTranslation();
const router = useRouter();
const { isDesktop } = useResponsiveStore();
const theme = useCunninghamTheme();
const colors = theme.colorsTokens();
const { mutate: createDoc } = useCreateDoc({
onSuccess: (doc) => {
router.push(`/docs/${doc.id}`);
},
});
const goToHome = () => {
router.push('/');
};
const createNewDoc = () => {
createDoc({ title: t('Untitled document') });
};
return (
<>
{isDesktop && (
<Box
data-testid="left-panel-desktop"
$css={`
height: calc(100vh - ${HEADER_HEIGHT}px);
width: 300px;
min-width: 300px;
border-right: 1px solid ${colors['greyscale-200']};
`}
>
<div>
<SeparatedSection>
<Box
$padding={{ horizontal: 'sm' }}
$direction="row"
$justify="space-between"
$align="center"
>
<Box $direction="row" $gap="2px">
<Button
onClick={goToHome}
size="medium"
color="primary-text"
icon={<Icon iconName="house" />}
/>
</Box>
<Button onClick={createNewDoc}>{t('New doc')}</Button>
</Box>
</SeparatedSection>
{children}
</div>
</Box>
)}
</>
);
};

View File

@@ -0,0 +1 @@
export * from './LeftPanel';

View File

@@ -4,33 +4,56 @@ import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Footer } from '@/features/footer';
import { Header } from '@/features/header';
import { HEADER_HEIGHT } from '@/features/header/conf';
import { LeftPanel } from '@/features/left-panel';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { useResponsiveStore } from '@/stores';
interface MainLayoutProps {
type MainLayoutProps = {
backgroundColor?: 'white' | 'grey';
withoutFooter?: boolean;
}
};
export function MainLayout({
children,
withoutFooter,
backgroundColor = 'white',
withoutFooter = false,
}: PropsWithChildren<MainLayoutProps>) {
const { isDesktop } = useResponsiveStore();
const { colorsTokens } = useCunninghamTheme();
const colors = colorsTokens();
return (
<Box>
<Box $minHeight="100vh">
<Header />
<Box $css="flex: 1;" $direction="row">
<Box
as="main"
$minHeight="100vh"
$width="100%"
$background={colorsTokens()['primary-bg']}
>
{children}
</Box>
<div>
<Header />
<Box $direction="row" $width="100%">
<LeftPanel />
<Box
as="main"
id={MAIN_LAYOUT_ID}
$align="center"
$flex={1}
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$padding={{
vertical: isDesktop ? 'base' : 'xs',
horizontal: isDesktop ? '6xl' : 'xs',
}}
$background={
backgroundColor === 'white'
? colors['greyscale-000']
: colors['greyscale-050']
}
$css={`
overflow-y: auto;
overflow-x: clip;
`}
>
{children}
</Box>
</Box>
{!withoutFooter && <Footer />}
</Box>
</div>
);
}

View File

@@ -0,0 +1 @@
export const MAIN_LAYOUT_ID = `mainContent`;

View File

@@ -33,6 +33,7 @@ export function DocLayout() {
<Head>
<meta name="robots" content="noindex" />
</Head>
<MainLayout withoutFooter>
<DocPage id={id} />
</MainLayout>

View File

@@ -1,15 +1,20 @@
import type { ReactElement } from 'react';
import { DocsGridContainer } from '@/features/docs/docs-grid';
import { Box } from '@/components';
import { DocsGrid } from '@/features/docs/docs-grid/components/DocsGrid';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
return <DocsGridContainer />;
return (
<Box $width="100%">
<DocsGrid />
</Box>
);
};
Page.getLayout = function getLayout(page: ReactElement) {
return <MainLayout>{page}</MainLayout>;
return <MainLayout backgroundColor="grey">{page}</MainLayout>;
};
export default Page;

View File

@@ -4,41 +4,67 @@ export type ScreenSize = 'small-mobile' | 'mobile' | 'tablet' | 'desktop';
export interface UseResponsiveStore {
isMobile: boolean;
isTablet: boolean;
isSmallMobile: boolean;
screenSize: ScreenSize;
screenWidth: number;
setScreenSize: (size: ScreenSize) => void;
isDesktop: boolean;
initializeResizeListener: () => () => void;
}
const initialState = {
isMobile: false,
isSmallMobile: false,
isTablet: false,
isDesktop: false,
screenSize: 'desktop' as ScreenSize,
screenWidth: 0,
};
export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
isMobile: initialState.isMobile,
isTablet: initialState.isTablet,
isSmallMobile: initialState.isSmallMobile,
screenSize: initialState.screenSize,
screenWidth: initialState.screenWidth,
setScreenSize: (size: ScreenSize) => set(() => ({ screenSize: size })),
isDesktop: initialState.isDesktop,
initializeResizeListener: () => {
const resizeHandler = () => {
const width = window.innerWidth;
if (width < 560) {
set({
isDesktop: false,
screenSize: 'small-mobile',
isMobile: true,
isTablet: false,
isSmallMobile: true,
});
} else if (width < 768) {
set({ screenSize: 'mobile', isMobile: true, isSmallMobile: false });
set({
isDesktop: false,
screenSize: 'mobile',
isTablet: false,
isMobile: true,
isSmallMobile: false,
});
} else if (width >= 768 && width < 1024) {
set({ screenSize: 'tablet', isMobile: false, isSmallMobile: false });
set({
isDesktop: false,
screenSize: 'tablet',
isTablet: true,
isMobile: false,
isSmallMobile: false,
});
} else {
set({ screenSize: 'desktop', isMobile: false, isSmallMobile: false });
set({
isDesktop: true,
screenSize: 'desktop',
isTablet: false,
isMobile: false,
isSmallMobile: false,
});
}
set({ screenWidth: width });