@@ -13,6 +13,7 @@ import { cn, titleToNumber } from '@/lib/utils'
13
13
import CodeBlock from './code-block'
14
14
import Link from 'next/link'
15
15
import { OpenInV0Button } from './open-in-v0'
16
+ import { isUrlCached } from '@/lib/serviceWorker'
16
17
17
18
export interface BlockPreviewProps {
18
19
code ?: string
@@ -29,11 +30,17 @@ const SMSIZE = 30
29
30
const MDSIZE = 62
30
31
const LGSIZE = 82
31
32
33
+ // Add this key function to generate cache keys
34
+ const getCacheKey = ( src : string ) => `iframe-cache-${ src } `
35
+
32
36
export const BlockPreview : React . FC < BlockPreviewProps > = ( { code, preview, title, category, previewOnly } ) => {
33
37
const [ width , setWidth ] = useState ( DEFAULTSIZE )
34
38
const [ mode , setMode ] = useState < 'preview' | 'code' > ( 'preview' )
35
39
const [ iframeHeight , setIframeHeight ] = useState ( 0 )
36
40
const [ isLoading , setIsLoading ] = useState ( true )
41
+ const [ shouldLoadIframe , setShouldLoadIframe ] = useState ( false )
42
+ const [ cachedHeight , setCachedHeight ] = useState < number | null > ( null )
43
+ const [ isIframeCached , setIsIframeCached ] = useState ( false )
37
44
38
45
const terminalCode = `pnpm dlx shadcn@canary add https://nsui.irung.me/r/${ category } -${ titleToNumber ( title ) } .json`
39
46
@@ -44,20 +51,120 @@ export const BlockPreview: React.FC<BlockPreviewProps> = ({ code, preview, title
44
51
const isLarge = useMedia ( '(min-width: 1024px)' )
45
52
46
53
const iframeRef = useRef < HTMLIFrameElement > ( null )
54
+ const observer = useRef < IntersectionObserver | null > ( null )
55
+ const blockRef = useRef < HTMLDivElement > ( null )
56
+
57
+ // Set up Intersection Observer to load iframe when it comes into view
58
+ useEffect ( ( ) => {
59
+ observer . current = new IntersectionObserver (
60
+ ( entries ) => {
61
+ if ( entries [ 0 ] . isIntersecting ) {
62
+ setShouldLoadIframe ( true )
63
+ observer . current ?. disconnect ( )
64
+ }
65
+ } ,
66
+ { threshold : 0.1 }
67
+ )
68
+
69
+ if ( blockRef . current ) {
70
+ observer . current . observe ( blockRef . current )
71
+ }
72
+
73
+ return ( ) => {
74
+ observer . current ?. disconnect ( )
75
+ }
76
+ } , [ ] )
77
+
78
+ // Check if the iframe content is already cached
79
+ useEffect ( ( ) => {
80
+ // Check if the iframe content is already cached by service worker
81
+ const checkCache = async ( ) => {
82
+ try {
83
+ const isCached = await isUrlCached ( preview )
84
+ setIsIframeCached ( isCached )
85
+ if ( isCached ) {
86
+ // If cached by service worker, we can load it immediately
87
+ setShouldLoadIframe ( true )
88
+ }
89
+ } catch ( error ) {
90
+ console . error ( 'Error checking cache status:' , error )
91
+ }
92
+ }
93
+
94
+ checkCache ( )
95
+
96
+ // Also check localStorage for cached height
97
+ try {
98
+ const cacheKey = getCacheKey ( preview )
99
+ const cached = localStorage . getItem ( cacheKey )
100
+ if ( cached ) {
101
+ const { height, timestamp } = JSON . parse ( cached )
102
+ // Use cached height if it's less than 24 hours old
103
+ const now = Date . now ( )
104
+ if ( now - timestamp < 24 * 60 * 60 * 1000 ) {
105
+ setCachedHeight ( height )
106
+ setIframeHeight ( height )
107
+ // Still load the iframe, but we can show the correct height immediately
108
+ }
109
+ }
110
+ } catch ( error ) {
111
+ console . error ( 'Error retrieving cache:' , error )
112
+ }
113
+ } , [ preview ] )
47
114
115
+ // Setup iframe load handler and height caching
48
116
useEffect ( ( ) => {
49
117
const iframe = iframeRef . current
118
+ if ( ! iframe || ! shouldLoadIframe ) return
119
+
50
120
const handleLoad = ( ) => {
51
121
setIsLoading ( false )
52
- const contentHeight = iframe ! . contentWindow ! . document . body . scrollHeight
53
- setIframeHeight ( contentHeight )
122
+
123
+ try {
124
+ const contentHeight = iframe . contentWindow ! . document . body . scrollHeight
125
+ setIframeHeight ( contentHeight )
126
+
127
+ // Cache the height in localStorage
128
+ const cacheKey = getCacheKey ( preview )
129
+ const cacheValue = JSON . stringify ( {
130
+ height : contentHeight ,
131
+ timestamp : Date . now ( ) ,
132
+ } )
133
+ localStorage . setItem ( cacheKey , cacheValue )
134
+ } catch ( e ) {
135
+ console . error ( 'Error accessing iframe content:' , e )
136
+ }
54
137
}
55
138
56
- iframe ! . addEventListener ( 'load' , handleLoad )
139
+ iframe . addEventListener ( 'load' , handleLoad )
57
140
return ( ) => {
58
- iframe ! . removeEventListener ( 'load' , handleLoad )
141
+ iframe . removeEventListener ( 'load' , handleLoad )
59
142
}
60
- } , [ ] )
143
+ } , [ shouldLoadIframe , preview ] )
144
+
145
+ // Add preload link for the iframe source when it's likely to be needed soon
146
+ useEffect ( ( ) => {
147
+ if ( ! blockRef . current || shouldLoadIframe ) return
148
+
149
+ // Create a preload link for the iframe content
150
+ const linkElement = document . createElement ( 'link' )
151
+ linkElement . rel = 'preload'
152
+ linkElement . href = preview
153
+ linkElement . as = 'document'
154
+
155
+ // Only add if not already in the document
156
+ if ( ! document . head . querySelector ( `link[rel="preload"][href="${ preview } "]` ) ) {
157
+ document . head . appendChild ( linkElement )
158
+ }
159
+
160
+ return ( ) => {
161
+ // Clean up the preload link when component unmounts or iframe loads
162
+ const existingLink = document . head . querySelector ( `link[rel="preload"][href="${ preview } "]` )
163
+ if ( existingLink ) {
164
+ document . head . removeChild ( existingLink )
165
+ }
166
+ }
167
+ } , [ preview , shouldLoadIframe ] )
61
168
62
169
return (
63
170
< section className = "group mb-16 border-b [--color-border:color-mix(in_oklab,var(--color-zinc-200)_75%,transparent)] dark:[--color-border:color-mix(in_oklab,var(--color-zinc-800)_60%,transparent)]" >
@@ -194,25 +301,31 @@ export const BlockPreview: React.FC<BlockPreviewProps> = ({ code, preview, title
194
301
defaultSize = { DEFAULTSIZE }
195
302
minSize = { SMSIZE }
196
303
className = "h-fit border-x" >
197
- < iframe
198
- key = { `${ category } -${ title } -iframe` }
199
- loading = "lazy"
200
- allowFullScreen
201
- ref = { iframeRef }
202
- title = { title }
203
- height = { iframeHeight }
204
- className = "h-(--iframe-height) @starting:opacity-0 @starting:blur-xl block min-h-56 w-full duration-200 will-change-auto"
205
- src = { preview }
206
- id = { `block-${ title } ` }
207
- style = { { '--iframe-height' : `${ iframeHeight } px` } as React . CSSProperties }
208
- { ...{ fetchPriority : 'low' } }
209
- />
210
-
211
- { isLoading && (
212
- < div className = "bg-background absolute inset-0 right-2 flex items-center justify-center border-x" >
213
- < div className = "border-primary size-6 animate-spin rounded-full border-2 border-t-transparent" />
214
- </ div >
215
- ) }
304
+ < div ref = { blockRef } >
305
+ { shouldLoadIframe ? (
306
+ < iframe
307
+ key = { `${ category } -${ title } -iframe` }
308
+ loading = { isIframeCached ? 'eager' : 'lazy' }
309
+ allowFullScreen
310
+ ref = { iframeRef }
311
+ title = { title }
312
+ height = { cachedHeight || iframeHeight }
313
+ className = { cn ( 'h-(--iframe-height) block min-h-56 w-full duration-200 will-change-auto' , ! cachedHeight && '@starting:opacity-0 @starting:blur-xl' , isIframeCached && '!opacity-100 !blur-none' ) }
314
+ src = { preview }
315
+ id = { `block-${ title } ` }
316
+ style = {
317
+ {
318
+ '--iframe-height' : `${ cachedHeight || iframeHeight } px` ,
319
+ display : 'block' ,
320
+ } as React . CSSProperties
321
+ }
322
+ />
323
+ ) : (
324
+ < div className = "flex min-h-56 items-center justify-center" >
325
+ < div className = "border-primary size-6 animate-spin rounded-full border-2 border-t-transparent" />
326
+ </ div >
327
+ ) }
328
+ </ div >
216
329
</ Panel >
217
330
218
331
{ isLarge && (
0 commit comments