first commit

This commit is contained in:
AyrisAI
2026-08-20 01:51:59 +03:00
commit 97b83c7fd4
109 changed files with 21215 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
import type { Metadata } from "next";
import { PublicMenuClient } from "@/components/PublicMenuClient";
import { DEMO_RESTAURANT, DEMO_CATEGORIES } from "@/lib/demo-data";
export const metadata: Metadata = {
title: "Gusto & Co. Brasserie | Menulio Canlı Demo Menü",
description: "Menulio dijital QR menü altyapısı ile çalışan örnek canlı restoran menüsü.",
};
type PageProps = {
searchParams?: Promise<{ theme?: string; table?: string }>;
};
export default async function DemoMenuPage({ searchParams }: PageProps) {
const resolved = searchParams ? await searchParams : {};
const theme = resolved.theme || "elegant";
return (
<PublicMenuClient
restaurant={DEMO_RESTAURANT}
categories={DEMO_CATEGORIES}
initialThemeKey={theme}
allowThemeSwitching={true}
/>
);
}
+37
View File
@@ -0,0 +1,37 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=Playfair+Display:ital,wght@0,600;0,700;1,600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
:root {
--font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-heading: 'Outfit', 'Plus Jakarta Sans', sans-serif;
--font-serif: 'Playfair Display', Georgia, serif;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
padding: 0;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Custom Scrollbar for Category Tabs */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
+16
View File
@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
export const metadata: Metadata = {
title: "Menulio | AI Destekli Yeni Nesil QR Menü",
description: "Restoranınız için yapay zeka destekli, modern dijital QR menü platformu.",
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="tr" suppressHydrationWarning>
<body suppressHydrationWarning>{children}</body>
</html>
);
}
+159
View File
@@ -0,0 +1,159 @@
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 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;
}
interface MenuCategoryRow {
id: string;
name: string;
description: string | null;
is_active: boolean;
menu_items: MenuItemRow[];
}
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 {
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] : [],
},
};
}
export default async function PublicMenuPage({ params, searchParams }: PageProps) {
const { slug } = await params;
const resolvedSearchParams = searchParams ? await searchParams : {};
const themeParam = resolvedSearchParams.theme;
// 1. Try finding restaurant by slug
let restaurant: any = null;
const { data: bySlug } = await supabase
.from("restaurants")
.select("id, name, slug, logo_url, phone, address")
.eq("slug", slug)
.maybeSingle();
if (bySlug) {
restaurant = bySlug;
} else {
// 2. Try finding restaurant by custom domain hostname
const { data: domainRow } = await supabase
.from("domains")
.select("restaurant_id, restaurants(id, name, slug, logo_url, phone, address)")
.eq("hostname", slug)
.maybeSingle();
if (domainRow?.restaurants) {
restaurant = domainRow.restaurants;
}
}
if (!restaurant) notFound();
const { 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) notFound();
const { data: rawCategories } = await supabase
.from("menu_categories")
.select("id, name, description, is_active, menu_items(id, name, description, price, image_url, is_active)")
.eq("menu_id", menu.id)
.eq("is_active", true)
.order("sort_order", { ascending: true })
.returns<MenuCategoryRow[]>();
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 categories: MenuCategory[] = (rawCategories ?? []).map((cat) => ({
id: cat.id,
name: cat.name,
description: cat.description,
is_active: cat.is_active,
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,
})),
}));
const restaurantData: RestaurantData = {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
logo_url: restaurant.logo_url,
phone: restaurant.phone,
address: restaurant.address,
};
return (
<PublicMenuClient
restaurant={restaurantData}
categories={categories}
initialThemeKey={themeKey}
customThemeConfig={customConfig}
/>
);
}
+308
View File
@@ -0,0 +1,308 @@
import type { Metadata } from "next";
import Link from "next/link";
import { THEME_PRESETS } from "@/lib/theme";
export const metadata: Metadata = {
title: "Menulio | AI Destekli Yeni Nesil QR Menü Platformu",
description:
"Restoran menünüzün fotoğrafını çekin, yapay zeka saniyeler içinde dijitalleştirsin. 5 lüks şablon, anında açılan mobil web menüsü ve dinamik QR kodlar.",
};
export default function HomePage() {
return (
<div className="min-h-screen bg-stone-950 text-stone-100 selection:bg-amber-500 selection:text-stone-950 font-sans">
{/* Navigation */}
<header className="border-b border-stone-800/80 bg-stone-950/75 backdrop-blur-xl sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-2xl bg-gradient-to-br from-amber-400 via-amber-600 to-amber-800 flex items-center justify-center font-black text-xl text-stone-950 shadow-lg shadow-amber-500/20">
M
</div>
<span className="font-extrabold text-2xl tracking-tight bg-gradient-to-r from-amber-200 via-amber-400 to-amber-500 bg-clip-text text-transparent">
MENULIO
</span>
</div>
<nav className="hidden md:flex items-center gap-8 text-sm font-medium text-stone-300">
<Link href="#features" className="hover:text-amber-400 transition-colors">
Özellikler
</Link>
<Link href="#templates" className="hover:text-amber-400 transition-colors">
Şablonlar (5 Tema)
</Link>
<Link href="#how-it-works" className="hover:text-amber-400 transition-colors">
Nasıl Çalışır?
</Link>
<Link href="/templates" className="hover:text-amber-400 transition-colors">
Canlı Demo
</Link>
</nav>
<div className="flex items-center gap-4">
<Link
href="/demo"
className="px-5 py-2.5 rounded-xl text-xs font-bold text-stone-200 bg-stone-900 hover:bg-stone-800 border border-stone-800 transition-all active:scale-95"
>
Menü Demosu Gör
</Link>
<Link
href="/templates"
className="px-5 py-2.5 rounded-xl text-xs font-bold text-stone-950 bg-gradient-to-r from-amber-400 to-amber-500 hover:from-amber-300 hover:to-amber-400 transition-all shadow-lg shadow-amber-500/25 active:scale-95"
>
Şablonları İncele
</Link>
</div>
</div>
</header>
{/* Hero Section */}
<section className="relative pt-24 pb-32 overflow-hidden px-6">
{/* Glow Effects */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-amber-500/10 rounded-full blur-[140px] pointer-events-none" />
<div className="absolute top-1/3 left-1/4 w-[400px] h-[400px] bg-blue-500/10 rounded-full blur-[120px] pointer-events-none" />
<div className="max-w-5xl mx-auto text-center space-y-8 relative z-10">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-amber-500/10 border border-amber-500/25 text-amber-400 text-xs font-bold tracking-wide animate-fade-in">
<span></span> YAPAY ZEKA DESTEKLİ DİJİTAL QR MENÜ SAAS
</div>
<h1 className="text-4xl sm:text-6xl lg:text-7xl font-extrabold tracking-tight leading-[1.1] text-stone-100">
Menünüzün Fotoğrafını Çekin,{" "}
<span className="bg-gradient-to-r from-amber-300 via-amber-400 to-amber-600 bg-clip-text text-transparent">
AI Saniyeler İçinde Dijitalleştirsin.
</span>
</h1>
<p className="text-lg sm:text-xl text-stone-400 max-w-3xl mx-auto leading-relaxed">
Menulio ile restoranınızın basılı menüsünü cep telefonunuzdan fotoğraflayın. Vision AI tüm
kategorileri, ürünleri ve fiyatları anında tanısın; 5 lüks şablondan birini seçip hemen
masalarınıza QR koyun.
</p>
{/* Call to Actions */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
<Link
href="/templates"
className="w-full sm:w-auto px-8 py-4 rounded-2xl font-bold text-stone-950 bg-gradient-to-r from-amber-400 via-amber-500 to-amber-600 hover:opacity-95 transition-all shadow-xl shadow-amber-500/25 text-base flex items-center justify-center gap-2"
>
<span>🌟</span> Canlı Şablonları Test Edin
</Link>
<Link
href="/demo"
className="w-full sm:w-auto px-8 py-4 rounded-2xl font-bold text-stone-200 bg-stone-900/90 hover:bg-stone-800 border border-stone-800 transition-all text-base flex items-center justify-center gap-2"
>
<span>📱</span> Canlı Demo Menüyü
</Link>
</div>
{/* Social Proof Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-6 pt-16 border-t border-stone-800/80 max-w-4xl mx-auto text-left sm:text-center">
<div>
<div className="text-3xl sm:text-4xl font-black text-amber-400">10 sn</div>
<div className="text-xs text-stone-400 mt-1">AI ile Menü Çıkarma</div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-white">5 Adet</div>
<div className="text-xs text-stone-400 mt-1">Özel Tasarım Şablonu</div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-amber-400">%100</div>
<div className="text-xs text-stone-400 mt-1">Dinamik QR (Yeniden Basılmaz)</div>
</div>
<div>
<div className="text-3xl sm:text-4xl font-black text-white">&lt;0.5 sn</div>
<div className="text-xs text-stone-400 mt-1">Mobil Açılış Hızı</div>
</div>
</div>
</div>
</section>
{/* 5 Menu Templates Showcase Section */}
<section id="templates" className="py-24 bg-stone-900/50 border-y border-stone-800/80 px-6">
<div className="max-w-7xl mx-auto space-y-16">
<div className="text-center max-w-3xl mx-auto space-y-4">
<span className="text-amber-400 text-xs font-bold uppercase tracking-wider">
TEMALAR & TASARIM ŞABLONLARI
</span>
<h2 className="text-3xl sm:text-5xl font-extrabold tracking-tight">
Her Restoran Konseptine Özel 5 Şablon
</h2>
<p className="text-stone-400 text-sm sm:text-base">
Fine dining'den gurme burgerciye, gece kulübünden butik kahveciye kadar tek tıkla şablon
değiştirin. Menü verileriniz bozulmadan sunum katmanı anında güncellenir.
</p>
</div>
{/* 5 Template Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Object.values(THEME_PRESETS).map((preset) => (
<div
key={preset.key}
className="rounded-3xl border border-stone-800 bg-stone-900/80 p-6 flex flex-col justify-between hover:border-amber-500/50 transition-all group shadow-xl"
>
<div className="space-y-4">
{/* Theme Header with Accent Color Dot */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="w-5 h-5 rounded-full shadow-md"
style={{ backgroundColor: preset.config.primaryColor }}
/>
<h3 className="text-xl font-bold text-white group-hover:text-amber-300 transition-colors">
{preset.name}
</h3>
</div>
<span className="text-[10px] font-bold px-2.5 py-1 rounded-full bg-stone-800 text-stone-300 border border-stone-700">
{preset.fontFamily.toUpperCase()}
</span>
</div>
{/* Theme Description */}
<p className="text-xs text-stone-400 leading-relaxed">
{preset.key === "elegant" && "Fine dining restoranlar, şarap evleri ve lüks mekanlar için altın & krem tonlarında zarif mizanpaj."}
{preset.key === "modern" && "Kafeler, burgerciler ve fast-casual mekanlar için canlı mavi safir tonları ve hızlı arama odaklı ızgara düzeni."}
{preset.key === "dark" && "Gece kulüpleri, kokteyl barlar ve steakhouse'lar için obsidian siyahı ve kehribar parıltılı lüks mod."}
{preset.key === "minimal" && "Butik kahveciler, fırınlar ve üçüncü nesil mekanlar için bol boşluklu sakin Nordic tipografi."}
{preset.key === "classic" && "Geleneksel brasserie'ler, meyhaneler ve trattoria'lar için Toskana bordo ve sıcak rustik doku."}
</p>
{/* Visual Preview Box */}
<div
className="h-32 rounded-2xl p-4 flex flex-col justify-between border shadow-inner relative overflow-hidden"
style={{
backgroundColor: preset.config.background,
borderColor: preset.cardBorder,
}}
>
<div className="flex items-center justify-between">
<div className="h-3 w-20 rounded bg-stone-400/40" />
<div
className="h-4 w-12 rounded-full"
style={{ backgroundColor: preset.config.primaryColor }}
/>
</div>
<div
className="p-2.5 rounded-xl border flex items-center justify-between"
style={{
backgroundColor: preset.cardBg,
borderColor: preset.cardBorder,
}}
>
<div className="space-y-1">
<div
className="h-2.5 w-24 rounded font-bold text-[10px]"
style={{ color: preset.textPrimary }}
>
Trüflü Burrata
</div>
<div className="h-2 w-16 rounded bg-stone-300/30" />
</div>
<div
className="text-xs font-black"
style={{ color: preset.config.primaryColor }}
>
₺420
</div>
</div>
</div>
</div>
<div className="pt-6 mt-4 border-t border-stone-800/80">
<Link
href={`/templates?theme=${preset.key}`}
className="w-full py-2.5 rounded-xl text-xs font-bold text-center block bg-stone-800 hover:bg-stone-700 text-stone-100 transition-all border border-stone-700"
>
{preset.name} Önizle →
</Link>
</div>
</div>
))}
</div>
<div className="text-center pt-4">
<Link
href="/templates"
className="inline-flex items-center gap-2 px-8 py-4 rounded-2xl font-bold text-stone-950 bg-amber-400 hover:bg-amber-300 shadow-xl shadow-amber-400/20 text-sm transition-all"
>
<span>📱</span> Tüm Şablonları Cihaz Simülatöründe Canlı Dene
</Link>
</div>
</div>
</section>
{/* How It Works Section */}
<section id="how-it-works" className="py-24 px-6 max-w-7xl mx-auto space-y-16">
<div className="text-center max-w-2xl mx-auto space-y-4">
<span className="text-amber-400 text-xs font-bold uppercase tracking-wider">
KOLAY ENTEGRASYON
</span>
<h2 className="text-3xl sm:text-5xl font-extrabold tracking-tight">
3 Kolay Adımda Masanızda
</h2>
<p className="text-stone-400 text-sm sm:text-base">
Saatlerce menü girmeye son. Tek yapmanız gereken cep telefonunuzla fotoğraf çekmek.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Step 1 */}
<div className="rounded-3xl border border-stone-800 bg-stone-900/60 p-8 space-y-4 relative">
<div className="w-12 h-12 rounded-2xl bg-amber-500/10 border border-amber-500/30 text-amber-400 flex items-center justify-center font-black text-xl">
1
</div>
<h3 className="text-xl font-bold text-white">Menü Fotoğrafını Çekin</h3>
<p className="text-stone-400 text-sm leading-relaxed">
Mevcut basılı menünüzün, broşürünüzün veya tahtanızın fotoğrafını Menulio mobil uygulamasıyla çekin.
</p>
</div>
{/* Step 2 */}
<div className="rounded-3xl border border-stone-800 bg-stone-900/60 p-8 space-y-4 relative">
<div className="w-12 h-12 rounded-2xl bg-amber-500/10 border border-amber-500/30 text-amber-400 flex items-center justify-center font-black text-xl">
2
</div>
<h3 className="text-xl font-bold text-white">AI Ayıklasın & Onaylayın</h3>
<p className="text-stone-400 text-sm leading-relaxed">
Vision AI tüm kategorileri, ürünleri ve fiyatları saniyeler içinde çıkarır. İnceleyin, istediğiniz temayı seçin.
</p>
</div>
{/* Step 3 */}
<div className="rounded-3xl border border-stone-800 bg-stone-900/60 p-8 space-y-4 relative">
<div className="w-12 h-12 rounded-2xl bg-amber-500/10 border border-amber-500/30 text-amber-400 flex items-center justify-center font-black text-xl">
3
</div>
<h3 className="text-xl font-bold text-white">QR Kodunuzu Masaya Koyun</h3>
<p className="text-stone-400 text-sm leading-relaxed">
Müşterileriniz uygulama indirmeden saniyeler içinde menüyü açsın. Fiyat değiştiğinde asla yeni QR basmayın.
</p>
</div>
</div>
</section>
{/* Footer */}
<footer className="border-t border-stone-800/80 bg-stone-950 py-12 px-6">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-6 text-xs text-stone-500">
<div className="flex items-center gap-3">
<span className="font-bold text-stone-300 text-sm tracking-wider">MENULIO</span>
<span>•</span>
<span>© {new Date().getFullYear()} Tüm hakları saklıdır.</span>
</div>
<div className="flex items-center gap-6">
<Link href="/templates" className="hover:text-stone-300 transition-colors">
Şablonlar
</Link>
<Link href="/demo" className="hover:text-stone-300 transition-colors">
Canlı Demo
</Link>
<Link href="/demo?theme=dark" className="hover:text-stone-300 transition-colors">
Dark Tema
</Link>
</div>
</div>
</footer>
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
import { NextResponse, type NextRequest } from "next/server";
import { supabase } from "@/lib/supabase";
type RouteParams = {
params: Promise<{ id: string }>;
};
// Stable QR redirect target (PRD §12) — printed QR codes encode this URL and
// never change; only the row's target_url is updated when domain/slug changes.
export async function GET(_req: NextRequest, { params }: RouteParams) {
const { id } = await params;
const { data } = await supabase.from("qr_codes").select("target_url").eq("id", id).maybeSingle();
if (!data) {
return NextResponse.json({ message: "not found" }, { status: 404 });
}
return NextResponse.redirect(data.target_url, { status: 302 });
}
+25
View File
@@ -0,0 +1,25 @@
import type { Metadata } from "next";
import { TemplateGalleryClient } from "@/components/TemplateGalleryClient";
import { DEMO_RESTAURANT, DEMO_CATEGORIES } from "@/lib/demo-data";
export const metadata: Metadata = {
title: "Menü Şablonları & Canlı Demo | Menulio",
description: "Menulio'nun 5 farklı lüks restoran şablonunu cihaz simülatöründe canlı olarak deneyimleyin.",
};
type PageProps = {
searchParams?: Promise<{ theme?: string }>;
};
export default async function TemplatesPage({ searchParams }: PageProps) {
const resolvedSearchParams = searchParams ? await searchParams : {};
const currentTheme = resolvedSearchParams.theme || "elegant";
return (
<TemplateGalleryClient
restaurant={DEMO_RESTAURANT}
categories={DEMO_CATEGORIES}
initialThemeKey={currentTheme}
/>
);
}