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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6ec4631e82
commit
df250c40e8
@@ -17,6 +17,7 @@ interface MenuItemRow {
|
|||||||
price: number;
|
price: number;
|
||||||
image_url: string | null;
|
image_url: string | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
translations: Record<string, { name: string; description: string | null }> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuCategoryRow {
|
interface MenuCategoryRow {
|
||||||
@@ -25,6 +26,7 @@ interface MenuCategoryRow {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
menu_items: MenuItemRow[];
|
menu_items: MenuItemRow[];
|
||||||
|
translations: Record<string, { name: string }> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||||
@@ -71,14 +73,14 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
async function fetchRestaurantBySlug(slug: string) {
|
async function fetchRestaurantBySlug(slug: string) {
|
||||||
let { data: bySlug, error } = await supabase
|
let { data: bySlug, error } = await supabase
|
||||||
.from("restaurants")
|
.from("restaurants")
|
||||||
.select("id, name, slug, logo_url, cover_url, phone, address")
|
.select("id, name, slug, logo_url, cover_url, phone, address, enabled_languages")
|
||||||
.eq("slug", slug)
|
.eq("slug", slug)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (error && error.code === "42703") {
|
if (error && error.code === "42703") {
|
||||||
const { data: fallback } = await supabase
|
const { data: fallback } = await supabase
|
||||||
.from("restaurants")
|
.from("restaurants")
|
||||||
.select("id, name, slug, logo_url, phone, address")
|
.select("id, name, slug, logo_url, phone, address, enabled_languages")
|
||||||
.eq("slug", slug)
|
.eq("slug", slug)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
@@ -90,14 +92,14 @@ async function fetchRestaurantBySlug(slug: string) {
|
|||||||
|
|
||||||
let { data: domainRow, error: domErr } = await supabase
|
let { data: domainRow, error: domErr } = await supabase
|
||||||
.from("domains")
|
.from("domains")
|
||||||
.select("restaurant_id, restaurants(id, name, slug, logo_url, cover_url, phone, address)")
|
.select("restaurant_id, restaurants(id, name, slug, logo_url, cover_url, phone, address, enabled_languages)")
|
||||||
.eq("hostname", slug)
|
.eq("hostname", slug)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (domErr && domErr.code === "42703") {
|
if (domErr && domErr.code === "42703") {
|
||||||
const { data: fallbackDom } = await supabase
|
const { data: fallbackDom } = await supabase
|
||||||
.from("domains")
|
.from("domains")
|
||||||
.select("restaurant_id, restaurants(id, name, slug, logo_url, phone, address)")
|
.select("restaurant_id, restaurants(id, name, slug, logo_url, phone, address, enabled_languages)")
|
||||||
.eq("hostname", slug)
|
.eq("hostname", slug)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
if (fallbackDom?.restaurants) {
|
if (fallbackDom?.restaurants) {
|
||||||
@@ -150,7 +152,9 @@ export default async function PublicMenuPage({ params, searchParams }: PageProps
|
|||||||
if (menu) {
|
if (menu) {
|
||||||
const { data: rawCategories } = await supabase
|
const { data: rawCategories } = await supabase
|
||||||
.from("menu_categories")
|
.from("menu_categories")
|
||||||
.select("id, name, description, is_active, menu_items(id, name, description, price, image_url, is_active)")
|
.select(
|
||||||
|
"id, name, description, is_active, translations, menu_items(id, name, description, price, image_url, is_active, translations)",
|
||||||
|
)
|
||||||
.eq("menu_id", menu.id)
|
.eq("menu_id", menu.id)
|
||||||
.eq("is_active", true)
|
.eq("is_active", true)
|
||||||
.order("sort_order", { ascending: true })
|
.order("sort_order", { ascending: true })
|
||||||
@@ -161,6 +165,7 @@ export default async function PublicMenuPage({ params, searchParams }: PageProps
|
|||||||
name: cat.name,
|
name: cat.name,
|
||||||
description: cat.description,
|
description: cat.description,
|
||||||
is_active: cat.is_active,
|
is_active: cat.is_active,
|
||||||
|
translations: cat.translations ?? {},
|
||||||
menu_items: (cat.menu_items ?? [])
|
menu_items: (cat.menu_items ?? [])
|
||||||
.filter((item) => item.is_active)
|
.filter((item) => item.is_active)
|
||||||
.map((item): MenuItem => ({
|
.map((item): MenuItem => ({
|
||||||
@@ -170,6 +175,7 @@ export default async function PublicMenuPage({ params, searchParams }: PageProps
|
|||||||
price: item.price,
|
price: item.price,
|
||||||
image_url: item.image_url,
|
image_url: item.image_url,
|
||||||
is_active: item.is_active,
|
is_active: item.is_active,
|
||||||
|
translations: item.translations ?? {},
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
} else if (slug === "demo" || slug === "gusto-brasserie") {
|
} else if (slug === "demo" || slug === "gusto-brasserie") {
|
||||||
@@ -202,6 +208,7 @@ export default async function PublicMenuPage({ params, searchParams }: PageProps
|
|||||||
categories={categories}
|
categories={categories}
|
||||||
initialThemeKey={themeKey}
|
initialThemeKey={themeKey}
|
||||||
customThemeConfig={customConfig}
|
customThemeConfig={customConfig}
|
||||||
|
enabledLanguages={restaurant.enabled_languages ?? ["tr"]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface MenuItem {
|
|||||||
price: number;
|
price: number;
|
||||||
image_url: string | null;
|
image_url: string | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
|
translations?: Record<string, { name: string; description: string | null }> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuCategory {
|
export interface MenuCategory {
|
||||||
@@ -20,8 +21,18 @@ export interface MenuCategory {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
menu_items: MenuItem[];
|
menu_items: MenuItem[];
|
||||||
|
translations?: Record<string, { name: string }> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LANGUAGE_LABELS: Record<string, string> = {
|
||||||
|
tr: "TR",
|
||||||
|
en: "EN",
|
||||||
|
de: "DE",
|
||||||
|
ar: "AR",
|
||||||
|
ru: "RU",
|
||||||
|
fr: "FR",
|
||||||
|
};
|
||||||
|
|
||||||
export interface RestaurantData {
|
export interface RestaurantData {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -38,6 +49,7 @@ interface PublicMenuClientProps {
|
|||||||
initialThemeKey?: string;
|
initialThemeKey?: string;
|
||||||
customThemeConfig?: Partial<ThemeConfig>;
|
customThemeConfig?: Partial<ThemeConfig>;
|
||||||
allowThemeSwitching?: boolean;
|
allowThemeSwitching?: boolean;
|
||||||
|
enabledLanguages?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PublicMenuClient({
|
export function PublicMenuClient({
|
||||||
@@ -46,8 +58,10 @@ export function PublicMenuClient({
|
|||||||
initialThemeKey = "elegant",
|
initialThemeKey = "elegant",
|
||||||
customThemeConfig,
|
customThemeConfig,
|
||||||
allowThemeSwitching = false,
|
allowThemeSwitching = false,
|
||||||
|
enabledLanguages = ["tr"],
|
||||||
}: PublicMenuClientProps) {
|
}: PublicMenuClientProps) {
|
||||||
const [selectedThemeKey, setSelectedThemeKey] = useState<string>(initialThemeKey);
|
const [selectedThemeKey, setSelectedThemeKey] = useState<string>(initialThemeKey);
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState<string>("tr");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [activeCategoryId, setActiveCategoryId] = useState<string>(categories[0]?.id ?? "");
|
const [activeCategoryId, setActiveCategoryId] = useState<string>(categories[0]?.id ?? "");
|
||||||
const [selectedItem, setSelectedItem] = useState<MenuItem | null>(null);
|
const [selectedItem, setSelectedItem] = useState<MenuItem | null>(null);
|
||||||
@@ -95,12 +109,27 @@ export function PublicMenuClient({
|
|||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, [categories]);
|
}, [categories]);
|
||||||
|
|
||||||
|
// Apply AI-translated text for the selected language, falling back to the
|
||||||
|
// original Turkish content when a translation isn't available yet.
|
||||||
|
const localizedCategories = useMemo(() => {
|
||||||
|
if (selectedLanguage === "tr") return categories;
|
||||||
|
return categories.map((cat) => ({
|
||||||
|
...cat,
|
||||||
|
name: cat.translations?.[selectedLanguage]?.name || cat.name,
|
||||||
|
menu_items: cat.menu_items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
name: item.translations?.[selectedLanguage]?.name || item.name,
|
||||||
|
description: item.translations?.[selectedLanguage]?.description ?? item.description,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}, [categories, selectedLanguage]);
|
||||||
|
|
||||||
// Filter categories and items based on search
|
// Filter categories and items based on search
|
||||||
const filteredCategories = useMemo(() => {
|
const filteredCategories = useMemo(() => {
|
||||||
const q = searchQuery.trim().toLowerCase();
|
const q = searchQuery.trim().toLowerCase();
|
||||||
if (!q) return categories;
|
if (!q) return localizedCategories;
|
||||||
|
|
||||||
return categories
|
return localizedCategories
|
||||||
.map((cat) => ({
|
.map((cat) => ({
|
||||||
...cat,
|
...cat,
|
||||||
menu_items: cat.menu_items.filter(
|
menu_items: cat.menu_items.filter(
|
||||||
@@ -110,7 +139,7 @@ export function PublicMenuClient({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
.filter((cat) => cat.menu_items.length > 0);
|
.filter((cat) => cat.menu_items.length > 0);
|
||||||
}, [categories, searchQuery]);
|
}, [localizedCategories, searchQuery]);
|
||||||
|
|
||||||
const totalItemsCount = useMemo(() => {
|
const totalItemsCount = useMemo(() => {
|
||||||
return categories.reduce((acc, cat) => acc + cat.menu_items.filter((i) => i.is_active).length, 0);
|
return categories.reduce((acc, cat) => acc + cat.menu_items.filter((i) => i.is_active).length, 0);
|
||||||
@@ -213,12 +242,33 @@ export function PublicMenuClient({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||||
|
{/* Language Selector Pills */}
|
||||||
|
{enabledLanguages.length > 1 && (
|
||||||
|
<div className="flex items-center gap-1 px-1 py-1 rounded-full bg-black/30 backdrop-blur-md border border-white/15">
|
||||||
|
{enabledLanguages.map((lang) => {
|
||||||
|
const isActive = selectedLanguage === lang;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={lang}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedLanguage(lang)}
|
||||||
|
className={`px-2 sm:px-2.5 py-1 rounded-full text-[10px] sm:text-[11px] font-bold tracking-wide transition-all ${
|
||||||
|
isActive ? "bg-white text-stone-900 shadow-sm" : "text-white/80 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{LANGUAGE_LABELS[lang] ?? lang.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Restaurant Info Trigger */}
|
{/* Restaurant Info Trigger */}
|
||||||
{(restaurant.phone || restaurant.address) && (
|
{(restaurant.phone || restaurant.address) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowInfoModal(true)}
|
onClick={() => setShowInfoModal(true)}
|
||||||
className="w-8 h-8 sm:w-9 sm:h-9 rounded-full bg-white/10 hover:bg-white/20 active:scale-95 transition-all flex items-center justify-center border border-white/15 backdrop-blur-md"
|
className="w-8 h-8 sm:w-9 sm:h-9 rounded-full bg-white/10 hover:bg-white/20 active:scale-95 transition-all flex items-center justify-center border border-white/15 backdrop-blur-md cursor-pointer focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:outline-none"
|
||||||
aria-label="Restoran Bilgisi"
|
aria-label="Restoran Bilgisi"
|
||||||
>
|
>
|
||||||
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -231,7 +281,7 @@ export function PublicMenuClient({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleShare}
|
onClick={handleShare}
|
||||||
className="w-8 h-8 sm:w-9 sm:h-9 rounded-full bg-white/10 hover:bg-white/20 active:scale-95 transition-all flex items-center justify-center border border-white/15 backdrop-blur-md"
|
className="w-8 h-8 sm:w-9 sm:h-9 rounded-full bg-white/10 hover:bg-white/20 active:scale-95 transition-all flex items-center justify-center border border-white/15 backdrop-blur-md cursor-pointer focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:outline-none"
|
||||||
aria-label="Menüyü Paylaş"
|
aria-label="Menüyü Paylaş"
|
||||||
>
|
>
|
||||||
{copiedLink ? (
|
{copiedLink ? (
|
||||||
@@ -330,8 +380,9 @@ export function PublicMenuClient({
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Yemek, içecek veya tatlı ara..."
|
placeholder="Yemek, içecek veya tatlı ara..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
|
aria-label="Menüde arama yap"
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-11 pr-10 py-3 rounded-2xl text-stone-900 bg-white/95 placeholder-stone-400 text-sm focus:outline-none focus:ring-2 transition-all shadow-lg"
|
className="w-full pl-11 pr-10 py-3 rounded-2xl text-stone-900 bg-white/95 placeholder-stone-400 text-sm focus:outline-none focus:ring-2 focus-visible:ring-amber-500 transition-all shadow-lg cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
outlineColor: theme.config.primaryColor,
|
outlineColor: theme.config.primaryColor,
|
||||||
}}
|
}}
|
||||||
@@ -410,7 +461,7 @@ export function PublicMenuClient({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar scroll-smooth py-0.5">
|
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar scroll-smooth py-0.5">
|
||||||
{categories.map((cat) => {
|
{localizedCategories.map((cat) => {
|
||||||
const isActive = activeCategoryId === cat.id;
|
const isActive = activeCategoryId === cat.id;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -608,6 +659,9 @@ export function PublicMenuClient({
|
|||||||
<img
|
<img
|
||||||
src={item.image_url}
|
src={item.image_url}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=800&auto=format&fit=crop&q=80";
|
||||||
|
}}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -687,6 +741,9 @@ export function PublicMenuClient({
|
|||||||
<img
|
<img
|
||||||
src={item.image_url}
|
src={item.image_url}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=800&auto=format&fit=crop&q=80";
|
||||||
|
}}
|
||||||
className="w-full h-full object-cover rounded-2xl shadow-md group-hover:scale-105 transition-transform duration-300"
|
className="w-full h-full object-cover rounded-2xl shadow-md group-hover:scale-105 transition-transform duration-300"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -715,6 +772,9 @@ export function PublicMenuClient({
|
|||||||
<img
|
<img
|
||||||
src={item.image_url}
|
src={item.image_url}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=800&auto=format&fit=crop&q=80";
|
||||||
|
}}
|
||||||
className="w-full h-full object-cover grayscale-[0.2] group-hover:grayscale-0 group-hover:scale-105 transition-all duration-500"
|
className="w-full h-full object-cover grayscale-[0.2] group-hover:grayscale-0 group-hover:scale-105 transition-all duration-500"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -794,6 +854,9 @@ export function PublicMenuClient({
|
|||||||
<img
|
<img
|
||||||
src={item.image_url}
|
src={item.image_url}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=800&auto=format&fit=crop&q=80";
|
||||||
|
}}
|
||||||
className="w-full h-full object-cover grayscale contrast-125 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-300"
|
className="w-full h-full object-cover grayscale contrast-125 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-300"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -820,6 +883,9 @@ export function PublicMenuClient({
|
|||||||
<img
|
<img
|
||||||
src={item.image_url}
|
src={item.image_url}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=800&auto=format&fit=crop&q=80";
|
||||||
|
}}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -909,6 +975,9 @@ export function PublicMenuClient({
|
|||||||
<img
|
<img
|
||||||
src={item.image_url}
|
src={item.image_url}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=800&auto=format&fit=crop&q=80";
|
||||||
|
}}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -926,23 +995,26 @@ export function PublicMenuClient({
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer
|
<footer
|
||||||
className="px-6 py-10 text-center border-t mt-auto text-xs space-y-4"
|
className="px-6 py-8 text-center border-t mt-auto text-xs space-y-3"
|
||||||
style={{
|
style={{
|
||||||
borderColor: theme.cardBorder,
|
borderColor: theme.cardBorder,
|
||||||
color: theme.textSecondary,
|
color: theme.textSecondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2 font-semibold">
|
<div className="flex items-center justify-center gap-2 font-bold tracking-wide">
|
||||||
<span>Powered by</span>
|
<span className="opacity-80">Powered by</span>
|
||||||
<span className="text-stone-900 dark:text-white font-black tracking-wider uppercase">
|
<span
|
||||||
|
className="font-black tracking-widest uppercase px-2 py-0.5 rounded text-[11px]"
|
||||||
|
style={{ backgroundColor: `${theme.config.primaryColor}20`, color: theme.config.primaryColor }}
|
||||||
|
>
|
||||||
MENULIO
|
MENULIO
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] opacity-70">
|
<p className="text-[11px] opacity-75 max-w-sm mx-auto">
|
||||||
© {new Date().getFullYear()} {restaurant.name}. Fiyatlara tüm vergiler dahildir.
|
© {new Date().getFullYear()} {restaurant.name}. Fiyatlara tüm vergiler dahildir.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<AyrisFooterBadge className="mt-4 pt-4 border-t border-black/10 dark:border-white/10" />
|
<AyrisFooterBadge className="pt-3 border-t border-black/5 dark:border-white/10" />
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* Floating Back to Top Button */}
|
{/* Floating Back to Top Button */}
|
||||||
@@ -973,6 +1045,9 @@ export function PublicMenuClient({
|
|||||||
<img
|
<img
|
||||||
src={selectedItem.image_url}
|
src={selectedItem.image_url}
|
||||||
alt={selectedItem.name}
|
alt={selectedItem.name}
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=800&auto=format&fit=crop&q=80";
|
||||||
|
}}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table restaurants
|
||||||
|
add column if not exists cover_url text;
|
||||||
Reference in New Issue
Block a user