diff --git a/apps/editor/app/editor/[projectId]/error.tsx b/apps/editor/app/editor/[projectId]/error.tsx
new file mode 100644
index 00000000..7ca2858b
--- /dev/null
+++ b/apps/editor/app/editor/[projectId]/error.tsx
@@ -0,0 +1,42 @@
+'use client'
+
+import Link from 'next/link'
+import { useEffect } from 'react'
+
+export default function EditorRouteError({
+ error,
+ reset,
+}: Readonly<{
+ error: Error & { digest?: string }
+ reset: () => void
+}>) {
+ useEffect(() => {
+ console.error('[editor-route] Unhandled editor error:', error)
+ }, [error])
+
+ return (
+
+
+
Editor error
+
+ We couldn't load this editor route. You can retry or return home.
+
+
+
+
+ Back to home
+
+
+
+
+ )
+}
diff --git a/apps/editor/app/viewer/[id]/error.tsx b/apps/editor/app/viewer/[id]/error.tsx
new file mode 100644
index 00000000..ef886307
--- /dev/null
+++ b/apps/editor/app/viewer/[id]/error.tsx
@@ -0,0 +1,42 @@
+'use client'
+
+import Link from 'next/link'
+import { useEffect } from 'react'
+
+export default function ViewerRouteError({
+ error,
+ reset,
+}: Readonly<{
+ error: Error & { digest?: string }
+ reset: () => void
+}>) {
+ useEffect(() => {
+ console.error('[viewer-route] Unhandled viewer error:', error)
+ }, [error])
+
+ return (
+
+
+
Viewer error
+
+ We couldn't load this project view. You can retry without leaving the app.
+
+
+
+
+ Back to home
+
+
+
+
+ )
+}
diff --git a/apps/editor/app/viewer/[id]/page.tsx b/apps/editor/app/viewer/[id]/page.tsx
index 8d06101b..bc173276 100644
--- a/apps/editor/app/viewer/[id]/page.tsx
+++ b/apps/editor/app/viewer/[id]/page.tsx
@@ -2,8 +2,11 @@
import { initSpatialGridSync, useScene } from '@pascal-app/core'
import { useViewer, Viewer } from '@pascal-app/viewer'
+import Link from 'next/link'
import { useParams } from 'next/navigation'
import { useEffect, useState } from 'react'
+import { ErrorBoundary } from '@/components/ui/primitives/error-boundary'
+import { SceneLoader } from '@/components/ui/scene-loader'
import {
getProjectModelPublic,
incrementProjectViews,
@@ -14,7 +17,34 @@ import { ViewerGuestCTA } from './viewer-guest-cta'
import { ViewerOverlay } from './viewer-overlay'
import { ViewerZoneSystem } from './viewer-zone-system'
-import { SceneLoader } from '@/components/ui/scene-loader'
+function ViewerSceneCrashFallback({ projectName }: { projectName?: string | null }) {
+ return (
+
+
+
The 3D scene failed to render
+
+ {projectName ? `"${projectName}" ` : ''}
+ hit a rendering error. The rest of the app is still available.
+
+
+
+
+ Back to home
+
+
+
+
+ )
+}
export default function ViewerPage() {
const params = useParams()
@@ -33,27 +63,46 @@ export default function ViewerPage() {
}, [projectId])
useEffect(() => {
+ let cancelled = false
+ setLoading(true)
+ setError(null)
+ setProjectId(null)
+ setProjectName(null)
+ setOwner(null)
+ setCanShowScans(true)
+ setCanShowGuides(true)
+ useViewer.getState().setShowScans(true)
+ useViewer.getState().setShowGuides(true)
+
const loadContent = async () => {
try {
// Check if it's a demo file (starts with 'demo_')
if (id.startsWith('demo_')) {
const response = await fetch(`/demos/${id}.json`)
+ if (cancelled) return
+
if (!response.ok) {
throw new Error(`Demo "${id}" not found`)
}
+
const data = await response.json()
+ if (cancelled) return
+
if (data.nodes && data.rootNodeIds) {
setScene(data.nodes, data.rootNodeIds)
initSpatialGridSync()
}
+
setProjectName('Demo')
} else {
// Load from database (public project)
const result = await getProjectModelPublic(id)
+ if (cancelled) return
if (result.success && result.data) {
const { project, model, isOwner } = result.data
const projectData = project as any
+
setProjectId(project.id)
setProjectName(project.name)
setOwner(projectData.owner ?? null)
@@ -81,19 +130,28 @@ export default function ViewerPage() {
// Increment view count
await incrementProjectViews(id)
+ if (cancelled) return
} else {
throw new Error(result.error || 'Project not found')
}
}
- setLoading(false)
+ if (!cancelled) {
+ setLoading(false)
+ }
} catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to load content')
- setLoading(false)
+ if (!cancelled) {
+ setError(err instanceof Error ? err.message : 'Failed to load content')
+ setLoading(false)
+ }
}
}
loadContent()
+
+ return () => {
+ cancelled = true
+ }
}, [id, setScene])
if (error) {
@@ -107,7 +165,7 @@ export default function ViewerPage() {
return (
{loading && }
-
+
{!loading && (
<>
+
+ }>
+
+
+
+
+
>
)}
-
-
-
-
-
)
}
diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx
index 0fe78eee..ff8af373 100644
--- a/apps/editor/components/editor/index.tsx
+++ b/apps/editor/components/editor/index.tsx
@@ -16,6 +16,7 @@ import { ToolManager } from '../tools/tool-manager'
import { ActionMenu } from '../ui/action-menu'
import { HelperManager } from '../ui/helpers/helper-manager'
import { PanelManager } from '../ui/panels/panel-manager'
+import { ErrorBoundary } from '../ui/primitives/error-boundary'
import { SidebarProvider } from '../ui/primitives/sidebar'
import { SceneLoader } from '../ui/scene-loader'
import { AppSidebar } from '../ui/sidebar/app-sidebar'
@@ -23,12 +24,11 @@ import { CustomCameraControls } from './custom-camera-controls'
import { ExportManager } from './export-manager'
import { FloatingActionMenu } from './floating-action-menu'
import { Grid } from './grid'
+import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
import { SelectionManager } from './selection-manager'
import { SiteEdgeLabels } from './site-edge-labels'
-import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
import { ThumbnailGenerator } from './thumbnail-generator'
-
// Load default scene initially (will be replaced when project loads)
useScene.getState().loadScene()
initSpatialGridSync()
@@ -66,6 +66,34 @@ interface EditorProps {
projectId?: string
}
+function EditorSceneCrashFallback() {
+ return (
+
+
+
The editor scene failed to render
+
+ You can retry the scene or return home without reloading the whole app shell.
+
+
+
+
+ Back to home
+
+
+
+
+ )
+}
+
export default function Editor({ projectId }: EditorProps) {
useKeyboard()
useProjectScene()
@@ -109,21 +137,23 @@ export default function Editor({ projectId }: EditorProps) {
-
-
-
-
- {/* Editor only system to toggle zone visibility */}
-
-
- {/* */}
-
-
-
-
-
-
-
+ }>
+
+
+
+
+ {/* Editor only system to toggle zone visibility */}
+
+
+ {/* */}
+
+
+
+
+
+
+
+
)
}
diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx
index 4840125c..ebbae641 100644
--- a/packages/viewer/src/components/viewer/index.tsx
+++ b/packages/viewer/src/components/viewer/index.tsx
@@ -64,20 +64,18 @@ interface ViewerProps {
selectionManager?: 'default' | 'custom'
}
-const Viewer: React.FC = ({
- children,
- selectionManager = 'default',
-}) => {
+const Viewer: React.FC = ({ children, selectionManager = 'default' }) => {
const theme = useViewer((state) => state.theme)
return (