fix(qr): guest menu 500 (SSR) + remove café discovery from owner panel

1. The /q/{code} guest menu returned HTTP 500 on every load. Root cause:
   menu-item-model-viewer.tsx did a top-level `import "@google/model-viewer"`,
   a browser-only lib that touches `self` at module evaluation. Next pulled
   it into the server module graph (page → qr-guest-menu → qr-menu-3d-sheet →
   model-viewer) and SSR crashed with "self is not defined". Now the library
   is imported lazily inside useEffect (client-only); a poster placeholder
   shows until the custom element registers. Verified /q/* now returns 200.

2. Removed the "discover" (browse other cafés) item from the café owner
   sidebar — café discovery belongs in Koja, not the owner panel. The owner
   still manages their OWN Koja listing from Settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-15 15:08:48 +03:30
parent 9765491f6f
commit 76d4434581
2 changed files with 47 additions and 4 deletions
@@ -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<unknown> | null = null;
function ensureModelViewerLoaded(): Promise<unknown> {
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 (
<div
className={className}
style={{
width: "100%",
height: "100%",
minHeight: "min(72vh, 420px)",
backgroundColor: "rgba(0,0,0,0.04)",
backgroundImage: poster ? `url(${poster})` : undefined,
backgroundSize: "contain",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
aria-label={alt}
role="img"
/>
);
}
return (
// @ts-expect-error model-viewer is a custom element from @google/model-viewer
<model-viewer
+2 -3
View File
@@ -21,7 +21,6 @@ import {
Wallet,
Clock,
LifeBuoy,
Compass,
} from "lucide-react";
export type NavGroupId = "main" | "customers" | "management";
@@ -39,7 +38,6 @@ export type NavItemKey =
| "coupons"
| "sms"
| "reviews"
| "discover"
| "inventory"
| "expenses"
| "shifts"
@@ -93,7 +91,8 @@ export const NAV_GROUPS: NavGroupDef[] = [
{ key: "coupons", href: "/coupons", icon: Ticket },
{ key: "sms", href: "/sms", icon: MessageSquare },
{ key: "reviews", href: "/reviews", icon: Star },
{ key: "discover", href: "/discover", icon: Compass },
// NOTE: café discovery (browsing other cafés) lives in Koja, not the owner
// panel. The owner manages their OWN Koja listing from Settings.
],
},
{