Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 14 additions & 16 deletions apps/master-sample-app/app/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,24 @@ export default function Page() {
const [documentId, setDocumentId] = useState<string>('')
const [isMounted, setIsMounted] = useState(false)
const isInitialized = useRef(false)

const currentSample = getSampleById(currentSampleId) || getDefaultSample()

// Helper function to get document ID for a specific demo
const getDocumentIdForDemo = useCallback((demoId: string): string => {
if (typeof window === 'undefined') return ''

// Check if there's a stored document ID for this demo
const stored = localStorage.getItem(`demo-${demoId}-document-id`)

const storageKey = `demo-${demoId}-document-id`
const stored = localStorage.getItem(storageKey)

if (stored) {
return stored
}

// Generate a new document ID for this demo
const newDocId = `doc-${demoId}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
localStorage.setItem(`demo-${demoId}-document-id`, newDocId)
localStorage.setItem(storageKey, newDocId)

return newDocId
}, [])

Expand Down Expand Up @@ -113,7 +115,7 @@ export default function Page() {
// Update URL after state update
const routePath = sample.metadata.routePath || window.location.pathname
const newUrl = `${routePath}?documentId=${docId}`

if (window.location.href !== window.location.origin + newUrl) {
window.history.pushState({}, '', newUrl)
}
Expand All @@ -122,9 +124,6 @@ export default function Page() {
}
}, [currentSampleId, getDocumentIdForDemo])

// Note: URL updates are now handled directly in handleSampleSelect and handleReset
// This effect is removed to prevent duplicate history entries

// Handle reset: generate new document ID for current demo
const handleReset = useCallback(() => {
if (typeof window === 'undefined') return
Expand All @@ -135,17 +134,17 @@ export default function Page() {

// Generate a new document ID for this specific demo
const newDocId = `doc-${currentSampleId}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`

// Update localStorage for this demo
localStorage.setItem(`demo-${currentSampleId}-document-id`, newDocId)

// Update state
setDocumentId(newDocId)

// Update URL
const routePath = sample.metadata.routePath || window.location.pathname
const newUrl = `${routePath}?documentId=${newDocId}`

if (window.location.href !== window.location.origin + newUrl) {
window.history.pushState({}, '', newUrl)
}
Expand Down Expand Up @@ -174,7 +173,7 @@ export default function Page() {
/>

{/* Main Content */}
<SampleViewer
<SampleViewer
sample={currentSample}
sidebarOpen={sidebarOpen}
onSidebarToggle={() => setSidebarOpen(!sidebarOpen)}
Expand All @@ -184,4 +183,3 @@ export default function Page() {
</div>
)
}

107 changes: 94 additions & 13 deletions apps/master-sample-app/components/viewer/iframe-pair.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
"use client"

import { useState } from "react"
import { useState, useRef, useEffect } from "react"

// Time to wait for Velt SDK auth to complete before refreshing
// validateclient takes ~7-8 seconds, add buffer
const VELT_AUTH_WAIT_MS = 8000

// Check if a demo origin has been initialized (Velt auth cached)
function isDemoInitialized(url: string): boolean {
try {
const origin = new URL(url).origin
const key = `velt-initialized-${origin}`
return localStorage.getItem(key) === 'true'
} catch {
return false
}
}

// Mark a demo origin as initialized
function markDemoInitialized(url: string): void {
try {
const origin = new URL(url).origin
const key = `velt-initialized-${origin}`
localStorage.setItem(key, 'true')
} catch {
// ignore
}
}

interface IframePairProps {
url?: string
Expand All @@ -19,6 +45,25 @@ export function IframePair({
const [isLoading2, setIsLoading2] = useState(true)
const [error1, setError1] = useState(false)
const [error2, setError2] = useState(false)
// If demo was already initialized, skip the wait/refresh cycle
const [hasRefreshed1, setHasRefreshed1] = useState(() => url ? isDemoInitialized(url) : false)
const [hasRefreshed2, setHasRefreshed2] = useState(() => {
const targetUrl = secondUrl || url
return targetUrl ? isDemoInitialized(targetUrl) : false
})

const iframe1Ref = useRef<HTMLIFrameElement>(null)
const iframe2Ref = useRef<HTMLIFrameElement>(null)
const refreshTimer1 = useRef<NodeJS.Timeout | null>(null)
const refreshTimer2 = useRef<NodeJS.Timeout | null>(null)

// Cleanup timers on unmount
useEffect(() => {
return () => {
if (refreshTimer1.current) clearTimeout(refreshTimer1.current)
if (refreshTimer2.current) clearTimeout(refreshTimer2.current)
}
}, [])

// Safety check: don't render if URL is not provided
if (!url) {
Expand All @@ -33,11 +78,45 @@ export function IframePair({

const handleIframeLoad = (iframeNum: number) => {
if (iframeNum === 1) {
setIsLoading1(false)
setError1(false)

if (!hasRefreshed1) {
// First load: wait for Velt auth to complete, then refresh
refreshTimer1.current = setTimeout(() => {
if (iframe1Ref.current && url) {
setHasRefreshed1(true)
// Force refresh by resetting src
iframe1Ref.current.src = url
}
}, VELT_AUTH_WAIT_MS)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale URL closure in timer causes wrong document

When the url prop changes (e.g., user clicks reset during the first 8-second wait), handleIframeLoad sets a new timer but doesn't cancel the existing one. The old timer's closure still captures the previous url value. When the old timer fires, it sets iframe.src to the stale URL, causing the iframe to briefly show the wrong document. The clearTimeout calls only exist in the unmount cleanup effect, not before setting new timers in handleIframeLoad.

Additional Locations (1)

Fix in Cursor Fix in Web

} else {
// Second load (after refresh): Velt auth should be cached, ready to show
setIsLoading1(false)
// Mark this demo origin as initialized for future visits
if (url) markDemoInitialized(url)
}
} else {
setIsLoading2(false)
setError2(false)

if (!hasRefreshed2) {
// First load: wait for Velt auth to complete, then refresh
refreshTimer2.current = setTimeout(() => {
if (iframe2Ref.current) {
const refreshUrl = secondUrl || url
if (refreshUrl) {
setHasRefreshed2(true)
// Force refresh by resetting src
iframe2Ref.current.src = refreshUrl
}
}
}, VELT_AUTH_WAIT_MS)
} else {
// Second load (after refresh): Velt auth should be cached, ready to show
setIsLoading2(false)
// Mark this demo origin as initialized for future visits
const targetUrl = secondUrl || url
if (targetUrl) markDemoInitialized(targetUrl)
}
}
}

Expand All @@ -56,9 +135,9 @@ export function IframePair({
return (
<div className="h-full relative">
<div className="rounded-lg border border-border bg-card overflow-hidden h-full">
{isLoading1 && !error1 && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
<div className="text-muted-foreground">Loading demo...</div>
{!hasRefreshed1 && (
<div className="absolute inset-0 flex items-center justify-center z-10 bg-background">
<div className="text-muted-foreground">Initializing...</div>
</div>
)}
{error1 && (
Expand All @@ -67,6 +146,7 @@ export function IframePair({
</div>
)}
<iframe
ref={iframe1Ref}
src={url}
className="w-full h-full"
style={{ maxHeight: height }}
Expand All @@ -84,9 +164,9 @@ export function IframePair({
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 h-full">
<div className="rounded-lg border border-border bg-card overflow-hidden relative">
{isLoading1 && !error1 && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
<div className="text-muted-foreground text-sm">Loading demo 1...</div>
{!hasRefreshed1 && (
<div className="absolute inset-0 flex items-center justify-center z-10 bg-background">
<div className="text-muted-foreground text-sm">Initializing demo...</div>
</div>
)}
{error1 && (
Expand All @@ -95,6 +175,7 @@ export function IframePair({
</div>
)}
<iframe
ref={iframe1Ref}
src={url}
className="w-full h-full"
style={{ maxHeight: height }}
Expand All @@ -105,9 +186,9 @@ export function IframePair({
/>
</div>
<div className="rounded-lg border border-border bg-card overflow-hidden relative">
{isLoading2 && !error2 && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
<div className="text-muted-foreground text-sm">Loading demo 2...</div>
{!hasRefreshed2 && (
<div className="absolute inset-0 flex items-center justify-center z-10 bg-background">
<div className="text-muted-foreground text-sm">Initializing demo...</div>
</div>
)}
{error2 && (
Expand All @@ -116,6 +197,7 @@ export function IframePair({
</div>
)}
<iframe
ref={iframe2Ref}
src={finalSecondUrl}
className="w-full h-full"
style={{ maxHeight: height }}
Expand All @@ -128,4 +210,3 @@ export function IframePair({
</div>
)
}

33 changes: 18 additions & 15 deletions apps/master-sample-app/components/viewer/sample-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export function SampleViewer({ sample, sidebarOpen, onSidebarToggle, documentId,
// Don't render iframes until documentId is ready AND URLs are computed
const isReady = !!documentId && !!iframeUrl

// IMPORTANT: Key should NOT include documentId to prevent remounts
// Only remount when sample changes, not when documentId changes
const iframeKey = sample.metadata.id

return (
<div
className="flex flex-1 flex-col transition-all duration-150 ease-in-out"
Expand All @@ -45,18 +49,18 @@ export function SampleViewer({ sample, sidebarOpen, onSidebarToggle, documentId,
}}
>
{/* Top Bar */}
<TopBar
mode={mode}
onModeChange={setMode}
sidebarOpen={sidebarOpen}
onSidebarToggle={onSidebarToggle}
title={sample.metadata.title}
githubUrl={sample.metadata.githubUrl}
routePath={sample.metadata.routePath}
documentId={documentId}
onReset={onReset}
iframeUrl={iframeUrl}
/>
<TopBar
mode={mode}
onModeChange={setMode}
sidebarOpen={sidebarOpen}
onSidebarToggle={onSidebarToggle}
title={sample.metadata.title}
githubUrl={sample.metadata.githubUrl}
routePath={sample.metadata.routePath}
documentId={documentId}
onReset={onReset}
iframeUrl={iframeUrl}
/>

{/* Content Area */}
<main className="flex-1 overflow-hidden p-4">
Expand All @@ -70,7 +74,7 @@ export function SampleViewer({ sample, sidebarOpen, onSidebarToggle, documentId,
secondUrl={iframeUrl2}
height="calc(100vh - 88px)"
displayMode={sample.metadata.displayMode || 'dual'}
key={`${sample.metadata.id}-${documentId}`}
key={iframeKey}
/>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 h-full">
Expand All @@ -86,12 +90,11 @@ export function SampleViewer({ sample, sidebarOpen, onSidebarToggle, documentId,
secondUrl={iframeUrl2}
height="100%"
displayMode={sample.metadata.displayMode || 'dual'}
key={`${sample.metadata.id}-${documentId}`}
key={`${iframeKey}-code`}
/>
</div>
)}
</main>
</div>
)
}

Loading