Public menu page now shows a TR|EN|... pill selector (styled after the Menulio design reference) that switches category/item text to the AI translations added via the mobile app, falling back to Turkish for anything not yet translated. Also adds a migration for restaurants.cover_url, which was referenced by existing code but was missing from the live database — the public menu page was silently forcing cover photos to null on every restaurant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
215 lines
6.4 KiB
TypeScript
215 lines
6.4 KiB
TypeScript
import type { Metadata } from "next";
|
||
import { notFound } from "next/navigation";
|
||
import { supabase } from "@/lib/supabase";
|
||
import { PublicMenuClient, type MenuCategory, type MenuItem, type RestaurantData } from "@/components/PublicMenuClient";
|
||
import { DEMO_RESTAURANT, DEMO_CATEGORIES } from "@/lib/demo-data";
|
||
import type { ThemeConfig } from "@menulio/shared";
|
||
|
||
type PageProps = {
|
||
params: Promise<{ slug: string }>;
|
||
searchParams?: Promise<{ theme?: string }>;
|
||
};
|
||
|
||
interface MenuItemRow {
|
||
id: string;
|
||
name: string;
|
||
description: string | null;
|
||
price: number;
|
||
image_url: string | null;
|
||
is_active: boolean;
|
||
translations: Record<string, { name: string; description: string | null }> | null;
|
||
}
|
||
|
||
interface MenuCategoryRow {
|
||
id: string;
|
||
name: string;
|
||
description: string | null;
|
||
is_active: boolean;
|
||
menu_items: MenuItemRow[];
|
||
translations: Record<string, { name: string }> | null;
|
||
}
|
||
|
||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||
const { slug } = await params;
|
||
let restaurant: any = null;
|
||
const { data: bySlug } = await supabase
|
||
.from("restaurants")
|
||
.select("name, logo_url")
|
||
.eq("slug", slug)
|
||
.maybeSingle();
|
||
|
||
if (bySlug) {
|
||
restaurant = bySlug;
|
||
} else if (slug === "demo" || slug === "gusto-brasserie") {
|
||
restaurant = DEMO_RESTAURANT;
|
||
} else {
|
||
const { data: domainRow } = await supabase
|
||
.from("domains")
|
||
.select("restaurants(name, logo_url)")
|
||
.eq("hostname", slug)
|
||
.maybeSingle();
|
||
if (domainRow?.restaurants) {
|
||
restaurant = domainRow.restaurants;
|
||
}
|
||
}
|
||
|
||
if (!restaurant) {
|
||
return {
|
||
title: "Menü Bulunamadı | Menulio",
|
||
};
|
||
}
|
||
|
||
return {
|
||
title: `${restaurant.name} | QR Menü`,
|
||
description: `${restaurant.name} restoranının güncel dijital QR menüsü ve fiyatları.`,
|
||
openGraph: {
|
||
title: `${restaurant.name} QR Menü`,
|
||
description: `${restaurant.name} dijital menüsünü inceleyin.`,
|
||
images: restaurant.logo_url ? [restaurant.logo_url] : [],
|
||
},
|
||
};
|
||
}
|
||
|
||
async function fetchRestaurantBySlug(slug: string) {
|
||
let { data: bySlug, error } = await supabase
|
||
.from("restaurants")
|
||
.select("id, name, slug, logo_url, cover_url, phone, address, enabled_languages")
|
||
.eq("slug", slug)
|
||
.maybeSingle();
|
||
|
||
if (error && error.code === "42703") {
|
||
const { data: fallback } = await supabase
|
||
.from("restaurants")
|
||
.select("id, name, slug, logo_url, phone, address, enabled_languages")
|
||
.eq("slug", slug)
|
||
.maybeSingle();
|
||
if (fallback) {
|
||
return { ...fallback, cover_url: null };
|
||
}
|
||
}
|
||
|
||
if (bySlug) return bySlug;
|
||
|
||
let { data: domainRow, error: domErr } = await supabase
|
||
.from("domains")
|
||
.select("restaurant_id, restaurants(id, name, slug, logo_url, cover_url, phone, address, enabled_languages)")
|
||
.eq("hostname", slug)
|
||
.maybeSingle();
|
||
|
||
if (domErr && domErr.code === "42703") {
|
||
const { data: fallbackDom } = await supabase
|
||
.from("domains")
|
||
.select("restaurant_id, restaurants(id, name, slug, logo_url, phone, address, enabled_languages)")
|
||
.eq("hostname", slug)
|
||
.maybeSingle();
|
||
if (fallbackDom?.restaurants) {
|
||
return { ...(fallbackDom.restaurants as any), cover_url: null };
|
||
}
|
||
}
|
||
|
||
return (domainRow?.restaurants as any) ?? null;
|
||
}
|
||
|
||
export default async function PublicMenuPage({ params, searchParams }: PageProps) {
|
||
const { slug } = await params;
|
||
const resolvedSearchParams = searchParams ? await searchParams : {};
|
||
const themeParam = resolvedSearchParams.theme;
|
||
|
||
const restaurant: any = await fetchRestaurantBySlug(slug);
|
||
|
||
if (!restaurant) {
|
||
if (slug === "demo" || slug === "gusto-brasserie") {
|
||
return (
|
||
<PublicMenuClient
|
||
restaurant={DEMO_RESTAURANT}
|
||
categories={DEMO_CATEGORIES}
|
||
initialThemeKey={themeParam || "coastal"}
|
||
/>
|
||
);
|
||
}
|
||
notFound();
|
||
}
|
||
|
||
// Find menu (published or any available menu)
|
||
let { data: menu } = await supabase
|
||
.from("menus")
|
||
.select("id, name, locations!inner(restaurant_id)")
|
||
.eq("locations.restaurant_id", restaurant.id)
|
||
.eq("is_published", true)
|
||
.maybeSingle();
|
||
|
||
if (!menu) {
|
||
const { data: anyMenu } = await supabase
|
||
.from("menus")
|
||
.select("id, name, locations!inner(restaurant_id)")
|
||
.eq("locations.restaurant_id", restaurant.id)
|
||
.maybeSingle();
|
||
menu = anyMenu;
|
||
}
|
||
|
||
let categories: MenuCategory[] = [];
|
||
|
||
if (menu) {
|
||
const { data: rawCategories } = await supabase
|
||
.from("menu_categories")
|
||
.select(
|
||
"id, name, description, is_active, translations, menu_items(id, name, description, price, image_url, is_active, translations)",
|
||
)
|
||
.eq("menu_id", menu.id)
|
||
.eq("is_active", true)
|
||
.order("sort_order", { ascending: true })
|
||
.returns<MenuCategoryRow[]>();
|
||
|
||
categories = (rawCategories ?? []).map((cat) => ({
|
||
id: cat.id,
|
||
name: cat.name,
|
||
description: cat.description,
|
||
is_active: cat.is_active,
|
||
translations: cat.translations ?? {},
|
||
menu_items: (cat.menu_items ?? [])
|
||
.filter((item) => item.is_active)
|
||
.map((item): MenuItem => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
description: item.description,
|
||
price: item.price,
|
||
image_url: item.image_url,
|
||
is_active: item.is_active,
|
||
translations: item.translations ?? {},
|
||
})),
|
||
}));
|
||
} else if (slug === "demo" || slug === "gusto-brasserie") {
|
||
categories = DEMO_CATEGORIES;
|
||
}
|
||
|
||
const { data: themeRow } = await supabase
|
||
.from("restaurant_themes")
|
||
.select("overrides, themes(key, config)")
|
||
.eq("restaurant_id", restaurant.id)
|
||
.maybeSingle();
|
||
|
||
const themeRelation = themeRow?.themes as unknown as { key?: string; config?: Partial<ThemeConfig> } | null;
|
||
const themeKey = themeParam || themeRelation?.key || "elegant";
|
||
const customConfig = (themeRow?.overrides as Partial<ThemeConfig>) || themeRelation?.config;
|
||
|
||
const restaurantData: RestaurantData = {
|
||
id: restaurant.id,
|
||
name: restaurant.name,
|
||
slug: restaurant.slug,
|
||
logo_url: restaurant.logo_url,
|
||
cover_url: restaurant.cover_url,
|
||
phone: restaurant.phone,
|
||
address: restaurant.address,
|
||
};
|
||
|
||
return (
|
||
<PublicMenuClient
|
||
restaurant={restaurantData}
|
||
categories={categories}
|
||
initialThemeKey={themeKey}
|
||
customThemeConfig={customConfig}
|
||
enabledLanguages={restaurant.enabled_languages ?? ["tr"]}
|
||
/>
|
||
);
|
||
}
|