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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user