Files
menulio/apps/web/src/app/menu/[slug]/page.tsx
T
AyrisAIandClaude Sonnet 5 df250c40e8 feat(web): public menu language pill selector; fix missing cover_url column
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>
2026-08-29 01:55:26 +03:00

215 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"]}
/>
);
}