Skip to content

Commit 8b73aa3

Browse files
committed
✨(frontend) create skeleton feature
creating a skeleton to be display during doc creation Signed-off-by: Cyril <c.gromoff@gmail.com>
1 parent dd56a8a commit 8b73aa3

File tree

13 files changed

+326
-8
lines changed

13 files changed

+326
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- ✨(frontend) create skeleton component for DocEditor #1491
12+
913
### Changed
1014

1115
- ♻️(frontend) adapt custom blocks to new implementation #1375

src/frontend/apps/impress/src/features/docs/doc-editor/components/DocEditor.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '@/docs/doc-management';
1414
import { TableContent } from '@/docs/doc-table-content/';
1515
import { Versions, useDocVersion } from '@/docs/doc-versioning/';
16+
import { useSkeletonStore } from '@/features/skeletons';
1617
import { useResponsiveStore } from '@/stores';
1718

1819
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
@@ -26,9 +27,16 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
2627
const { isDesktop } = useResponsiveStore();
2728
const isVersion = !!versionId && typeof versionId === 'string';
2829
const { provider, isReady } = useProviderStore();
30+
const { setIsSkeletonVisible } = useSkeletonStore();
31+
const isProviderReady = isReady && provider;
2932

30-
// TODO: Use skeleton instead of loading
31-
if (!provider || !isReady) {
33+
useEffect(() => {
34+
if (isProviderReady) {
35+
setIsSkeletonVisible(false);
36+
}
37+
}, [isProviderReady, setIsSkeletonVisible]);
38+
39+
if (!isProviderReady) {
3240
return <Loading />;
3341
}
3442

src/frontend/apps/impress/src/features/docs/doc-management/api/useCreateDoc.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ export const createDoc = async (): Promise<Doc> => {
2020

2121
interface CreateDocProps {
2222
onSuccess: (data: Doc) => void;
23+
onError?: (error: APIError) => void;
2324
}
2425

25-
export function useCreateDoc({ onSuccess }: CreateDocProps) {
26+
export function useCreateDoc({ onSuccess, onError }: CreateDocProps) {
2627
const queryClient = useQueryClient();
2728
return useMutation<Doc, APIError>({
2829
mutationFn: createDoc,
@@ -32,5 +33,8 @@ export function useCreateDoc({ onSuccess }: CreateDocProps) {
3233
});
3334
onSuccess(data);
3435
},
36+
onError: (error) => {
37+
onError?.(error);
38+
},
3539
});
3640
}

src/frontend/apps/impress/src/features/docs/doc-management/components/DocPage403.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { Button } from '@openfun/cunningham-react';
22
import Head from 'next/head';
33
import Image from 'next/image';
4+
import { useEffect } from 'react';
45
import { useTranslation } from 'react-i18next';
56
import styled from 'styled-components';
67

78
import img403 from '@/assets/icons/icon-403.png';
89
import { Box, Icon, Loading, StyledLink, Text } from '@/components';
910
import { ButtonAccessRequest } from '@/docs/doc-share';
1011
import { useDocAccessRequests } from '@/docs/doc-share/api/useDocAccessRequest';
12+
import { useSkeletonStore } from '@/features/skeletons';
1113

1214
const StyledButton = styled(Button)`
1315
width: fit-content;
@@ -19,6 +21,13 @@ interface DocProps {
1921

2022
export const DocPage403 = ({ id }: DocProps) => {
2123
const { t } = useTranslation();
24+
const { setIsSkeletonVisible } = useSkeletonStore();
25+
26+
useEffect(() => {
27+
// Ensure the skeleton overlay is hidden on 403 page
28+
setIsSkeletonVisible(false);
29+
}, [setIsSkeletonVisible]);
30+
2231
const {
2332
data: requests,
2433
isLoading: isLoadingRequest,

src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeaderButton.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,59 @@
11
import { Button } from '@openfun/cunningham-react';
22
import { useRouter } from 'next/router';
3+
import { useState } from 'react';
34
import { useTranslation } from 'react-i18next';
45

56
import { Icon } from '@/components';
67
import { useCreateDoc } from '@/features/docs/doc-management';
8+
import { useSkeletonStore } from '@/features/skeletons';
79

810
import { useLeftPanelStore } from '../stores';
911

1012
export const LeftPanelHeaderButton = () => {
1113
const router = useRouter();
1214
const { t } = useTranslation();
1315
const { togglePanel } = useLeftPanelStore();
16+
const { setIsSkeletonVisible } = useSkeletonStore();
17+
const [isNavigating, setIsNavigating] = useState(false);
18+
1419
const { mutate: createDoc, isPending: isDocCreating } = useCreateDoc({
1520
onSuccess: (doc) => {
16-
void router.push(`/docs/${doc.id}`);
17-
togglePanel();
21+
setIsNavigating(true);
22+
// Wait for navigation to complete
23+
router
24+
.push(`/docs/${doc.id}`)
25+
.then(() => {
26+
// The skeleton will be disabled by the [id] page once the data is loaded
27+
setIsNavigating(false);
28+
togglePanel();
29+
})
30+
.catch(() => {
31+
// In case of navigation error, disable the skeleton
32+
setIsSkeletonVisible(false);
33+
setIsNavigating(false);
34+
});
35+
},
36+
onError: () => {
37+
// If there's an error, disable the skeleton
38+
setIsSkeletonVisible(false);
39+
setIsNavigating(false);
1840
},
1941
});
42+
43+
const handleClick = () => {
44+
setIsSkeletonVisible(true);
45+
createDoc();
46+
};
47+
48+
const isLoading = isDocCreating || isNavigating;
49+
2050
return (
2151
<Button
2252
data-testid="new-doc-button"
2353
color="primary"
24-
onClick={() => createDoc()}
54+
onClick={handleClick}
2555
icon={<Icon $variation="000" iconName="add" aria-hidden="true" />}
26-
disabled={isDocCreating}
56+
disabled={isLoading}
2757
>
2858
{t('New doc')}
2959
</Button>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { css, keyframes } from 'styled-components';
2+
3+
import { Box, BoxType } from '@/components';
4+
import { useCunninghamTheme } from '@/cunningham';
5+
import { useResponsiveStore } from '@/stores';
6+
7+
const shimmer = keyframes`
8+
0% {
9+
background-position: -1000px 0;
10+
}
11+
100% {
12+
background-position: 1000px 0;
13+
}
14+
`;
15+
16+
type SkeletonLineProps = Partial<BoxType>;
17+
18+
type SkeletonCircleProps = Partial<BoxType>;
19+
20+
export const DocEditorSkeleton = () => {
21+
const { isDesktop } = useResponsiveStore();
22+
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
23+
24+
const SkeletonLine = ({ $css, ...props }: SkeletonLineProps) => {
25+
return (
26+
<Box
27+
$width="100%"
28+
$height="16px"
29+
$css={css`
30+
background: linear-gradient(
31+
90deg,
32+
${colorsTokens['greyscale-100']} 0%,
33+
${colorsTokens['greyscale-200']} 50%,
34+
${colorsTokens['greyscale-100']} 100%
35+
);
36+
background-size: 1000px 100%;
37+
animation: ${shimmer} 2s infinite linear;
38+
border-radius: 4px;
39+
${$css}
40+
`}
41+
{...props}
42+
/>
43+
);
44+
};
45+
46+
const SkeletonCircle = ({ $css, ...props }: SkeletonCircleProps) => {
47+
return (
48+
<Box
49+
$width="32px"
50+
$height="32px"
51+
$css={css`
52+
background: linear-gradient(
53+
90deg,
54+
${colorsTokens['greyscale-100']} 0%,
55+
${colorsTokens['greyscale-200']} 50%,
56+
${colorsTokens['greyscale-100']} 100%
57+
);
58+
background-size: 1000px 100%;
59+
animation: ${shimmer} 2s infinite linear;
60+
border-radius: 50%;
61+
${$css}
62+
`}
63+
{...props}
64+
/>
65+
);
66+
};
67+
68+
return (
69+
<>
70+
{/* Main Editor Container */}
71+
<Box
72+
$maxWidth="868px"
73+
$width="100%"
74+
$height="100%"
75+
className="--docs--doc-editor-skeleton"
76+
>
77+
{/* Header Skeleton */}
78+
<Box
79+
$padding={{ horizontal: isDesktop ? '70px' : 'base' }}
80+
className="--docs--doc-editor-header-skeleton"
81+
>
82+
<Box
83+
$width="100%"
84+
$padding={{ top: isDesktop ? '65px' : 'md' }}
85+
$gap={spacingsTokens['base']}
86+
>
87+
<Box
88+
$direction="row"
89+
$align="center"
90+
$width="100%"
91+
$padding={{ bottom: 'xs' }}
92+
>
93+
<Box
94+
$direction="row"
95+
$justify="space-between"
96+
$css="flex:1;"
97+
$gap="0.5rem 1rem"
98+
$align="center"
99+
$maxWidth="100%"
100+
>
101+
{/* Title and metadata skeleton */}
102+
<Box $gap="0.25rem" $css="flex:1;">
103+
{/* Title - "Document sans titre" style */}
104+
<SkeletonLine $width="35%" $height="40px" />
105+
106+
{/* Metadata (role and last update) */}
107+
<Box $direction="row" $gap="0.5rem" $align="center">
108+
<SkeletonLine $maxWidth="260px" $height="12px" />
109+
</Box>
110+
</Box>
111+
112+
{/* Toolbox skeleton (buttons) */}
113+
<Box $direction="row" $gap="0.75rem" $align="center">
114+
{/* Partager button */}
115+
<SkeletonLine $width="90px" $height="40px" />
116+
{/* Download icon */}
117+
<SkeletonCircle $width="40px" $height="40px" />
118+
{/* Menu icon */}
119+
<SkeletonCircle $width="40px" $height="40px" />
120+
</Box>
121+
</Box>
122+
</Box>
123+
124+
{/* Separator */}
125+
<SkeletonLine $height="1px" />
126+
</Box>
127+
</Box>
128+
129+
{/* Content Skeleton */}
130+
<Box
131+
$direction="row"
132+
$width="100%"
133+
$css="overflow-x: clip; flex: 1;"
134+
$position="relative"
135+
className="--docs--doc-editor-content-skeleton"
136+
>
137+
<Box
138+
$css="flex:1;"
139+
$position="relative"
140+
$width="100%"
141+
$padding={{ horizontal: isDesktop ? '70px' : 'base', top: 'lg' }}
142+
>
143+
{/* Placeholder text similar to screenshot */}
144+
<Box $gap="0rem">
145+
{/* Single placeholder line like in the screenshot */}
146+
<SkeletonLine $width="85%" $height="20px" />
147+
</Box>
148+
</Box>
149+
</Box>
150+
</Box>
151+
</>
152+
);
153+
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { PropsWithChildren, useEffect, useRef, useState } from 'react';
2+
import { css, keyframes } from 'styled-components';
3+
4+
import { Box } from '@/components';
5+
import { useCunninghamTheme } from '@/cunningham';
6+
import { useSkeletonStore } from '@/features/skeletons';
7+
8+
const FADE_DURATION_MS = 250;
9+
10+
const fadeOut = keyframes`
11+
from {
12+
opacity: 1;
13+
}
14+
to {
15+
opacity: 0;
16+
}
17+
`;
18+
19+
export const Skeleton = ({ children }: PropsWithChildren) => {
20+
const { isSkeletonVisible } = useSkeletonStore();
21+
const { colorsTokens } = useCunninghamTheme();
22+
const [isVisible, setIsVisible] = useState(isSkeletonVisible);
23+
const [isFadingOut, setIsFadingOut] = useState(true);
24+
const timeoutVisibleRef = useRef<NodeJS.Timeout | null>(null);
25+
26+
useEffect(() => {
27+
if (isSkeletonVisible) {
28+
setIsVisible(true);
29+
setIsFadingOut(false);
30+
} else {
31+
setIsFadingOut(true);
32+
if (!timeoutVisibleRef.current) {
33+
timeoutVisibleRef.current = setTimeout(() => {
34+
setIsVisible(false);
35+
}, FADE_DURATION_MS * 2);
36+
}
37+
}
38+
39+
return () => {
40+
if (timeoutVisibleRef.current) {
41+
clearTimeout(timeoutVisibleRef.current);
42+
timeoutVisibleRef.current = null;
43+
}
44+
};
45+
}, [isSkeletonVisible]);
46+
47+
if (!isVisible) {
48+
return null;
49+
}
50+
51+
return (
52+
<Box
53+
className="--docs--skeleton"
54+
$align="center"
55+
$width="100%"
56+
$height="100%"
57+
$background={colorsTokens['greyscale-000']}
58+
$css={css`
59+
position: absolute;
60+
inset: 0;
61+
z-index: 999;
62+
overflow: hidden;
63+
will-change: opacity;
64+
animation: ${isFadingOut && fadeOut} ${FADE_DURATION_MS}ms ease-in-out
65+
forwards;
66+
`}
67+
>
68+
{children}
69+
</Box>
70+
);
71+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './DocEditorSkeleton';
2+
export * from './Skeleton';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './components';
2+
export * from './store';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useSkeletonStore';

0 commit comments

Comments
 (0)