diff --git a/web/dashboard/src/components/menu/menu-item-model-viewer.tsx b/web/dashboard/src/components/menu/menu-item-model-viewer.tsx index 4ce5660..3a1786e 100644 --- a/web/dashboard/src/components/menu/menu-item-model-viewer.tsx +++ b/web/dashboard/src/components/menu/menu-item-model-viewer.tsx @@ -1,6 +1,6 @@ "use client"; -import "@google/model-viewer"; +import { useEffect, useState } from "react"; import { resolveMediaUrl } from "@/lib/api/client"; type MenuItemModelViewerProps = { @@ -10,16 +10,60 @@ type MenuItemModelViewerProps = { className?: string; }; +// `@google/model-viewer` references browser globals (`self`) at module load, so a +// top-level `import` crashes server-side rendering (e.g. the /q guest menu). +// Load it lazily on the client only, once, the first time a viewer mounts. +let modelViewerLoad: Promise | null = null; +function ensureModelViewerLoaded(): Promise { + if (typeof window === "undefined") return Promise.resolve(); + modelViewerLoad ??= import("@google/model-viewer"); + return modelViewerLoad; +} + export function MenuItemModelViewer({ modelUrl, posterUrl, alt, className, }: MenuItemModelViewerProps) { + const [ready, setReady] = useState(false); + + useEffect(() => { + let active = true; + void ensureModelViewerLoaded().then(() => { + if (active) setReady(true); + }); + return () => { + active = false; + }; + }, []); + const src = resolveMediaUrl(modelUrl); const poster = posterUrl ? resolveMediaUrl(posterUrl) : undefined; if (!src) return null; + // Until the custom element is registered, show the poster (or an empty box) so + // layout is stable and nothing references the browser-only library on the server. + if (!ready) { + return ( +
+ ); + } + return ( // @ts-expect-error model-viewer is a custom element from @google/model-viewer