1- import React from "react" ;
1+ import React , { useEffect , useState } from "react" ;
2+ import { useInView } from "react-intersection-observer" ;
23
34import { RichTextRenderer } from "../RichTextRenderer" ;
45import { BlockRenderer } from "../BlockRenderer" ;
56
7+ const ImagePlaceholderIcon : React . FC = ( ) => (
8+ < svg
9+ width = "24"
10+ height = "24"
11+ viewBox = "0 0 24 24"
12+ fill = "none"
13+ stroke = "#9ca3af"
14+ strokeWidth = "2"
15+ strokeLinecap = "round"
16+ strokeLinejoin = "round"
17+ >
18+ < rect x = "3" y = "3" width = "18" height = "18" rx = "2" ry = "2" />
19+ < circle cx = "9" cy = "9" r = "2" />
20+ < path d = "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
21+ </ svg >
22+ ) ;
23+
624interface ImageBlockRendererProps extends React . HTMLAttributes < HTMLDivElement > {
725 block : any ;
826 depth ?: number ;
927 components ?: React . ComponentProps < typeof BlockRenderer > [ "components" ] ;
28+ resolveImageUrl ?: ( url : string ) => Promise < string > ;
1029}
1130
1231export const ImageBlockRenderer : React . FC < ImageBlockRendererProps > = ( {
1332 block,
1433 depth = 0 ,
1534 className,
1635 components,
36+ resolveImageUrl,
1737 ...props
1838} ) => {
1939 const imageData = block . image ;
40+ const [ url , setUrl ] = useState < string > ( ) ;
41+ const [ isLoading , setIsLoading ] = useState < boolean > ( false ) ;
42+ const [ hasError , setHasError ] = useState < boolean > ( false ) ;
43+ const { ref, inView } = useInView ( { threshold : 0.1 , triggerOnce : true } ) ;
2044
2145 const getImageUrl = ( ) => {
2246 if ( imageData ?. type === "external" ) {
@@ -29,6 +53,38 @@ export const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
2953
3054 const imageUrl = getImageUrl ( ) ;
3155
56+ useEffect ( ( ) => {
57+ let cancelled = false ;
58+
59+ const imageUrlEffect = async ( ) => {
60+ if ( resolveImageUrl && imageUrl ) {
61+ setIsLoading ( true ) ;
62+ setHasError ( false ) ;
63+ try {
64+ const url_ = await resolveImageUrl ( imageUrl ) ;
65+ if ( ! cancelled ) {
66+ setUrl ( url_ ) ;
67+ setIsLoading ( false ) ;
68+ }
69+ } catch ( error ) {
70+ if ( ! cancelled ) {
71+ setHasError ( true ) ;
72+ setIsLoading ( false ) ;
73+ console . error ( "Failed to resolve image URL:" , error ) ;
74+ }
75+ }
76+ }
77+ } ;
78+
79+ if ( inView && imageUrl ) {
80+ imageUrlEffect ( ) ;
81+ }
82+
83+ return ( ) => {
84+ cancelled = true ;
85+ } ;
86+ } , [ inView , imageUrl , resolveImageUrl ] ) ;
87+
3288 return (
3389 < div
3490 { ...props }
@@ -37,24 +93,47 @@ export const ImageBlockRenderer: React.FC<ImageBlockRendererProps> = ({
3793 >
3894 < div className = "notion-selectable-container" >
3995 < div role = "figure" >
40- < div className = "notion-cursor-default" >
41- < div >
42- { imageUrl && (
43- < img
44- alt = ""
45- src = { imageUrl }
46- style = { { maxWidth : "100%" , height : "auto" } }
47- />
48- ) }
49- </ div >
96+ < div className = "notion-cursor-default" ref = { ref } >
97+ { imageUrl && (
98+ < div
99+ style = { {
100+ position : "relative" ,
101+ width : "100%" ,
102+ maxWidth : "600px" ,
103+ } }
104+ >
105+ { ( isLoading || ( ! url && resolveImageUrl ) ) && ! hasError && (
106+ < div className = "image-loading-placeholder" >
107+ < div className = "image-loading-content" >
108+ < div className = "image-loading-icon" >
109+ < ImagePlaceholderIcon />
110+ </ div >
111+ < div className = "image-loading-text" > Loading image...</ div >
112+ </ div >
113+ </ div >
114+ ) }
115+ { hasError && (
116+ < div className = "image-error-placeholder" >
117+ < div className = "image-error-text" > Failed to load image</ div >
118+ </ div >
119+ ) }
120+ { ! isLoading && ! hasError && ( url || ! resolveImageUrl ) && (
121+ < img
122+ alt = { imageData ?. caption ? "" : "Image" }
123+ src = { url || imageUrl }
124+ onError = { ( ) => setHasError ( true ) }
125+ />
126+ ) }
127+ </ div >
128+ ) }
50129 </ div >
51130 { /* Caption */ }
52131 { imageData ?. caption && imageData . caption . length > 0 && (
53- < div >
132+ < figcaption className = "notion-image-caption" >
54133 < div className = "notranslate" >
55134 < RichTextRenderer richText = { imageData . caption } />
56135 </ div >
57- </ div >
136+ </ figcaption >
58137 ) }
59138 </ div >
60139 </ div >
0 commit comments