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";
|
"use client";
|
||||||
|
|
||||||
import "@google/model-viewer";
|
import { useEffect, useState } from "react";
|
||||||
import { resolveMediaUrl } from "@/lib/api/client";
|
import { resolveMediaUrl } from "@/lib/api/client";
|
||||||
|
|
||||||
type MenuItemModelViewerProps = {
|
type MenuItemModelViewerProps = {
|
||||||
@@ -10,16 +10,60 @@ type MenuItemModelViewerProps = {
|
|||||||
className?: string;
|
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({
|
export function MenuItemModelViewer({
|
||||||
modelUrl,
|
modelUrl,
|
||||||
posterUrl,
|
posterUrl,
|
||||||
alt,
|
alt,
|
||||||
className,
|
className,
|
||||||
}: MenuItemModelViewerProps) {
|
}: 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 src = resolveMediaUrl(modelUrl);
|
||||||
const poster = posterUrl ? resolveMediaUrl(posterUrl) : undefined;
|
const poster = posterUrl ? resolveMediaUrl(posterUrl) : undefined;
|
||||||
if (!src) return null;
|
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 (
|
return (
|
||||||
// @ts-expect-error model-viewer is a custom element from @google/model-viewer
|
// @ts-expect-error model-viewer is a custom element from @google/model-viewer
|
||||||
<model-viewer
|
<model-viewer
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
Wallet,
|
Wallet,
|
||||||
Clock,
|
Clock,
|
||||||
LifeBuoy,
|
LifeBuoy,
|
||||||
Compass,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export type NavGroupId = "main" | "customers" | "management";
|
export type NavGroupId = "main" | "customers" | "management";
|
||||||
@@ -39,7 +38,6 @@ export type NavItemKey =
|
|||||||
| "coupons"
|
| "coupons"
|
||||||
| "sms"
|
| "sms"
|
||||||
| "reviews"
|
| "reviews"
|
||||||
| "discover"
|
|
||||||
| "inventory"
|
| "inventory"
|
||||||
| "expenses"
|
| "expenses"
|
||||||
| "shifts"
|
| "shifts"
|
||||||
@@ -93,7 +91,8 @@ export const NAV_GROUPS: NavGroupDef[] = [
|
|||||||
{ key: "coupons", href: "/coupons", icon: Ticket },
|
{ key: "coupons", href: "/coupons", icon: Ticket },
|
||||||
{ key: "sms", href: "/sms", icon: MessageSquare },
|
{ key: "sms", href: "/sms", icon: MessageSquare },
|
||||||
{ key: "reviews", href: "/reviews", icon: Star },
|
{ 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